@poodle64/ui 2026.9.6 → 2026.9.7
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/README.md +65 -0
- package/dist/components/feedback/index.d.ts +2 -0
- package/dist/components/feedback/index.js +2 -0
- package/dist/components/feedback/report-widget.svelte +174 -0
- package/dist/components/feedback/report-widget.svelte.d.ts +7 -0
- package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/select/select.svelte.d.ts +1 -1
- package/dist/telemetry.d.ts +40 -0
- package/dist/telemetry.js +63 -0
- package/package.json +12 -1
- package/registry/component-map.json +1 -1
- package/registry/component-map.md +1 -1
package/README.md
CHANGED
|
@@ -33,11 +33,15 @@ src/lib/
|
|
|
33
33
|
utils.ts cn() (clsx + tailwind-merge) and the shared TS helper types
|
|
34
34
|
format.ts the Australian value formatters: money, dates, percentages,
|
|
35
35
|
numbers — en-AU, AUD, Australia/Brisbane
|
|
36
|
+
telemetry.ts browser telemetry: one initBrowserTelemetry() call, one Faro
|
|
37
|
+
instance, one session id (see below)
|
|
36
38
|
styles.css the component stylesheet: scale keys, .ds-edge, .ds-chip/.ds-dot,
|
|
37
39
|
the dialogue-section divider rule
|
|
38
40
|
components/ui/ one directory per component: the shadcn-svelte primitives
|
|
39
41
|
(bits-ui behaviour + shadcn markup/variants) and the composed
|
|
40
42
|
page-chrome components built on top of them
|
|
43
|
+
components/feedback/ ReportWidget: the fixed bottom-right "report a problem"
|
|
44
|
+
trigger and dialogue every app mounts (see below)
|
|
41
45
|
dist/ generated by `pnpm build` (@sveltejs/package); never edit
|
|
42
46
|
```
|
|
43
47
|
|
|
@@ -1089,6 +1093,67 @@ one is bound to Xero tax codes besides. Relative time ("2 hours ago") is out
|
|
|
1089
1093
|
too — it rides on `date-fns` in the one app that has it, and a display formatter
|
|
1090
1094
|
is not worth making that a dependency of every consumer.
|
|
1091
1095
|
|
|
1096
|
+
## Browser telemetry and the feedback widget
|
|
1097
|
+
|
|
1098
|
+
Two exports are app machinery rather than page chrome: `@poodle64/ui/telemetry`
|
|
1099
|
+
(Faro) and `@poodle64/ui/feedback` (`ReportWidget`). A household app mounts both
|
|
1100
|
+
once, in its client entry and root layout, and every app mounts the same pair.
|
|
1101
|
+
|
|
1102
|
+
```ts
|
|
1103
|
+
// +layout.ts or the client entry — the browser leg only
|
|
1104
|
+
import { initBrowserTelemetry } from '@poodle64/ui/telemetry';
|
|
1105
|
+
|
|
1106
|
+
initBrowserTelemetry({
|
|
1107
|
+
app: 'cadmus',
|
|
1108
|
+
version: APP_VERSION,
|
|
1109
|
+
url: PUBLIC_FARO_URL
|
|
1110
|
+
});
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
`initBrowserTelemetry` is inert without a `url` — a private or unconfigured
|
|
1114
|
+
deployment ships no beacons and gets null from `getSessionId()`, with no branch
|
|
1115
|
+
at the call site. It is idempotent: a second call returns the first instance,
|
|
1116
|
+
so an HMR leg or a remounted layout cannot produce a second Faro global. The
|
|
1117
|
+
configuration is the house decision, not an app decision: session tracking on,
|
|
1118
|
+
the console captured nowhere (the console stays local by rule), and the
|
|
1119
|
+
tracing instrumentation on so every same-origin `fetch`/XHR carries a
|
|
1120
|
+
`traceparent` — the feedback POST and the page's server-side traces share one
|
|
1121
|
+
trace id. The faro packages are `dependencies`, not peers, for the same reason
|
|
1122
|
+
`bits-ui` is a peer: a duplicated Faro global would strand the session id the
|
|
1123
|
+
feedback widget reports against.
|
|
1124
|
+
|
|
1125
|
+
`ReportWidget` is the user-facing other half: a fixed bottom-right trigger
|
|
1126
|
+
opening the one report dialogue — a labelled "What happened?" textarea, and a
|
|
1127
|
+
screenshot checkbox (on by default) that captures `document.body` with
|
|
1128
|
+
`html-to-image`. A capture the page cannot produce is tolerated silently: the
|
|
1129
|
+
report sends with a null image rather than not at all. Submitting POSTs one
|
|
1130
|
+
JSON body to the `endpoint` prop (default `/api/feedback`):
|
|
1131
|
+
|
|
1132
|
+
```json
|
|
1133
|
+
{
|
|
1134
|
+
"message": "string — the textarea, required",
|
|
1135
|
+
"route": "string — location.pathname at send time",
|
|
1136
|
+
"user_agent": "string — navigator.userAgent",
|
|
1137
|
+
"viewport": { "width": 1024, "height": 768 },
|
|
1138
|
+
"session_id": "string | null — Faro's session id",
|
|
1139
|
+
"screenshot": "string | null — a PNG data URL, or null"
|
|
1140
|
+
}
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
That body is the interface the app's own route is written against, and
|
|
1144
|
+
`src/test/report-widget.test.ts` asserts it field for field. The endpoint
|
|
1145
|
+
answers `{ "id": "…" }`; the dialogue shows the id with a thank-you (quote it
|
|
1146
|
+
to follow up) and fires `onSubmitted(id)` so the app can log or route on it.
|
|
1147
|
+
A failure keeps the draft and offers Retry — the message the user typed is
|
|
1148
|
+
the hard half to reproduce, and losing it to a flaky connection would make
|
|
1149
|
+
the retry cost more than the first attempt.
|
|
1150
|
+
|
|
1151
|
+
The widget lives under `components/feedback/`, not `components/ui/`: it is
|
|
1152
|
+
mounted by the layout, once, alongside the telemetry init — not composed into
|
|
1153
|
+
routes by situation, so it stands outside the situation→component registry
|
|
1154
|
+
for the same reason. `getSessionId()` reading null when telemetry is off is
|
|
1155
|
+
ordinary, not an error: the report sends with a null `session_id`.
|
|
1156
|
+
|
|
1092
1157
|
## The gate: `ds-check-colour-surface`
|
|
1093
1158
|
|
|
1094
1159
|
The package ships the gate that proves its own surface actually resolves in the
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// In the module script so the instance script's typed state can reference
|
|
3
|
+
// it: svelte2tsx emits a component's instance-script type declarations
|
|
4
|
+
// below its `let`s, so a type both declared and referenced there turns
|
|
5
|
+
// into "used before its declaration".
|
|
6
|
+
type WidgetState = 'idle' | 'submitting' | 'done' | 'error';
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { toPng } from 'html-to-image';
|
|
11
|
+
import Check from '@lucide/svelte/icons/check';
|
|
12
|
+
import CircleAlert from '@lucide/svelte/icons/circle-alert';
|
|
13
|
+
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
|
14
|
+
import MessageSquarePlus from '@lucide/svelte/icons/message-square-plus';
|
|
15
|
+
import AppDialog from '../ui/app-dialog/app-dialog.svelte';
|
|
16
|
+
import { Button } from '../ui/button/index.js';
|
|
17
|
+
import { Checkbox } from '../ui/checkbox/index.js';
|
|
18
|
+
import { Label } from '../ui/label/index.js';
|
|
19
|
+
import Textarea from '../ui/textarea/textarea.svelte';
|
|
20
|
+
import { getSessionId } from '../../telemetry';
|
|
21
|
+
|
|
22
|
+
// The feedback widget every household app mounts: a fixed bottom-right
|
|
23
|
+
// trigger opening the one report dialogue. It is deliberately NOT under
|
|
24
|
+
// components/ui — it is app machinery the layout mounts once (alongside
|
|
25
|
+
// initBrowserTelemetry), not page chrome a route composes from, so it does
|
|
26
|
+
// not sit in the situation→component registry either.
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
/** Where the report POSTs. The app's own route, defaulting to the one every household app serves. */
|
|
30
|
+
endpoint = '/api/feedback',
|
|
31
|
+
/** Fired with the id the endpoint returned, once, on the success leg. */
|
|
32
|
+
onSubmitted
|
|
33
|
+
}: {
|
|
34
|
+
endpoint?: string;
|
|
35
|
+
onSubmitted?: (id: string) => void;
|
|
36
|
+
} = $props();
|
|
37
|
+
|
|
38
|
+
let open = $state(false);
|
|
39
|
+
let message = $state('');
|
|
40
|
+
let includeScreenshot = $state(true);
|
|
41
|
+
let phase: WidgetState = $state('idle');
|
|
42
|
+
let submittedId: string | null = $state(null);
|
|
43
|
+
|
|
44
|
+
// Instance-scoped element ids: a label's `for` has to reach its control,
|
|
45
|
+
// and a page that mounts two widgets must not cross-wire their fields.
|
|
46
|
+
const uid = Math.random().toString(36).slice(2, 10);
|
|
47
|
+
const formId = `feedback-form-${uid}`;
|
|
48
|
+
const messageId = `feedback-message-${uid}`;
|
|
49
|
+
const screenshotId = `feedback-screenshot-${uid}`;
|
|
50
|
+
|
|
51
|
+
async function submit() {
|
|
52
|
+
phase = 'submitting';
|
|
53
|
+
|
|
54
|
+
// A screenshot the page cannot produce (canvas-tainted content, a
|
|
55
|
+
// browser without the API) is tolerated silently: the report still
|
|
56
|
+
// sends, with a null image rather than no report at all.
|
|
57
|
+
let screenshot: string | null = null;
|
|
58
|
+
if (includeScreenshot) {
|
|
59
|
+
screenshot = await toPng(document.body).catch(() => null);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(endpoint, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
message,
|
|
68
|
+
route: location.pathname,
|
|
69
|
+
user_agent: navigator.userAgent,
|
|
70
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
71
|
+
session_id: getSessionId(),
|
|
72
|
+
screenshot
|
|
73
|
+
})
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) throw new Error(`the feedback endpoint answered ${response.status}`);
|
|
77
|
+
|
|
78
|
+
const { id } = (await response.json()) as { id: string };
|
|
79
|
+
submittedId = id;
|
|
80
|
+
phase = 'done';
|
|
81
|
+
onSubmitted?.(id);
|
|
82
|
+
} catch {
|
|
83
|
+
// The draft survives the failure — the message the user typed is
|
|
84
|
+
// the hard half to reproduce, and losing it on a flaky connection
|
|
85
|
+
// would make the retry cost more than the first attempt.
|
|
86
|
+
phase = 'error';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function reset() {
|
|
91
|
+
// A sent report clears the dialogue for the next one; an abandoned
|
|
92
|
+
// draft is kept, so an accidental close is not lost work.
|
|
93
|
+
if (phase === 'done') {
|
|
94
|
+
message = '';
|
|
95
|
+
submittedId = null;
|
|
96
|
+
}
|
|
97
|
+
phase = 'idle';
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<!-- The floating trigger is the same shape the context column's is: a raw
|
|
102
|
+
button in the house chrome rather than the Button primitive, because a
|
|
103
|
+
Button's padding and text rhythm are for a row of actions, not a
|
|
104
|
+
44px disc pinned over the page. -->
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onclick={() => (open = true)}
|
|
108
|
+
class="border-border bg-card text-muted-foreground hover:text-foreground ds-edge fixed right-4 bottom-[calc(1rem+env(safe-area-inset-bottom))] z-50 grid size-11 flex-none place-items-center rounded-full border transition-colors"
|
|
109
|
+
aria-label="Report a problem"
|
|
110
|
+
>
|
|
111
|
+
<MessageSquarePlus class="size-4.5" />
|
|
112
|
+
<span class="sr-only">Report a problem</span>
|
|
113
|
+
</button>
|
|
114
|
+
|
|
115
|
+
<AppDialog
|
|
116
|
+
bind:open
|
|
117
|
+
title="Report a problem"
|
|
118
|
+
subtitle="Describe what happened and the household team will take a look."
|
|
119
|
+
size="sm"
|
|
120
|
+
onOpenChange={(next) => !next && reset()}
|
|
121
|
+
>
|
|
122
|
+
{#if phase === 'done'}
|
|
123
|
+
<div class="flex flex-col items-start gap-3" data-testid="feedback-done">
|
|
124
|
+
<span class="bg-primary/15 text-primary grid size-8 place-items-center rounded-md">
|
|
125
|
+
<Check class="size-4" />
|
|
126
|
+
</span>
|
|
127
|
+
<p class="font-medium">Thank you — your report was received.</p>
|
|
128
|
+
<p class="text-muted-foreground text-sm">
|
|
129
|
+
Its reference is <span class="text-foreground font-medium" data-testid="feedback-id">{submittedId}</span
|
|
130
|
+
>. Quote it if you follow it up.
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
{:else}
|
|
134
|
+
<form id={formId} onsubmit={(e) => { e.preventDefault(); submit(); }}>
|
|
135
|
+
<div class="space-y-2">
|
|
136
|
+
<Label for={messageId}>What happened?</Label>
|
|
137
|
+
<Textarea
|
|
138
|
+
id={messageId}
|
|
139
|
+
bind:value={message}
|
|
140
|
+
placeholder="What did you see, and what did you expect?"
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="mt-4 flex items-center gap-2">
|
|
144
|
+
<Checkbox id={screenshotId} bind:checked={includeScreenshot} />
|
|
145
|
+
<Label for={screenshotId}>Include a screenshot of this page</Label>
|
|
146
|
+
</div>
|
|
147
|
+
{#if phase === 'error'}
|
|
148
|
+
<p class="text-destructive mt-4 flex items-center gap-2 text-sm" role="alert" data-testid="feedback-error">
|
|
149
|
+
<CircleAlert class="size-4 flex-none" />
|
|
150
|
+
Your report could not be sent. Check your connection and try again.
|
|
151
|
+
</p>
|
|
152
|
+
{/if}
|
|
153
|
+
</form>
|
|
154
|
+
{/if}
|
|
155
|
+
|
|
156
|
+
{#snippet footer()}
|
|
157
|
+
{#if phase === 'done'}
|
|
158
|
+
<Button variant="outline" onclick={() => (open = false)}>Close</Button>
|
|
159
|
+
{:else if phase === 'error'}
|
|
160
|
+
<Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
|
|
161
|
+
<Button type="submit" form={formId} disabled={!message.trim()}>Retry</Button>
|
|
162
|
+
{:else}
|
|
163
|
+
<Button variant="outline" disabled={phase === 'submitting'} onclick={() => (open = false)}>
|
|
164
|
+
Cancel
|
|
165
|
+
</Button>
|
|
166
|
+
<Button type="submit" form={formId} disabled={phase === 'submitting' || !message.trim()}>
|
|
167
|
+
{#if phase === 'submitting'}
|
|
168
|
+
<LoaderCircle class="size-4 animate-spin" />
|
|
169
|
+
{/if}
|
|
170
|
+
{phase === 'submitting' ? 'Sending…' : 'Send report'}
|
|
171
|
+
</Button>
|
|
172
|
+
{/if}
|
|
173
|
+
{/snippet}
|
|
174
|
+
</AppDialog>
|
|
@@ -9,6 +9,6 @@ type $$ComponentProps = WithoutChildrenOrChild<DialogPrimitive.RootProps> & With
|
|
|
9
9
|
showCloseButton?: boolean;
|
|
10
10
|
class?: string;
|
|
11
11
|
};
|
|
12
|
-
declare const CommandDialog: import("svelte").Component<$$ComponentProps, {}, "
|
|
12
|
+
declare const CommandDialog: import("svelte").Component<$$ComponentProps, {}, "open" | "ref" | "value">;
|
|
13
13
|
type CommandDialog = ReturnType<typeof CommandDialog>;
|
|
14
14
|
export default CommandDialog;
|
|
@@ -2,7 +2,7 @@ declare const InputGroupInput: import("svelte").Component<(Omit<import("svelte/e
|
|
|
2
2
|
type: "file";
|
|
3
3
|
files?: FileList;
|
|
4
4
|
} | {
|
|
5
|
-
type?: "number" | "month" | "hidden" | "color" | "submit" | "reset" | "button" | "search" | "checkbox" | "radio" | (string & {}) | "text" | "tel" | "
|
|
5
|
+
type?: "number" | "month" | "url" | "hidden" | "color" | "submit" | "reset" | "button" | "search" | "checkbox" | "radio" | (string & {}) | "text" | "tel" | "email" | "date" | "time" | "image" | "datetime-local" | "password" | "range" | "week";
|
|
6
6
|
files?: undefined;
|
|
7
7
|
})) & {
|
|
8
8
|
ref?: HTMLElement | null | undefined;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
declare const Select: import("svelte").Component<import("bits-ui").SelectRootPropsWithoutHTML, {}, "
|
|
1
|
+
declare const Select: import("svelte").Component<import("bits-ui").SelectRootPropsWithoutHTML, {}, "open" | "value">;
|
|
2
2
|
type Select = ReturnType<typeof Select>;
|
|
3
3
|
export default Select;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser telemetry for a household frontend: one `initBrowserTelemetry()`
|
|
3
|
+
* call in the client entry, one Faro instance for the app's lifetime.
|
|
4
|
+
*
|
|
5
|
+
* Both faro packages are ordinary `dependencies`, not peers: telemetry is not
|
|
6
|
+
* an optional flavour a consumer chooses per route, it is part of what every
|
|
7
|
+
* household app mounts, and a duplicated faro global would strand the session
|
|
8
|
+
* id the feedback widget reports against.
|
|
9
|
+
*/
|
|
10
|
+
import { type Faro } from '@grafana/faro-web-sdk';
|
|
11
|
+
/** What an app supplies to switch telemetry on. */
|
|
12
|
+
export interface BrowserTelemetryOptions {
|
|
13
|
+
/** The app's name, as it should appear in Faro's app metadata. */
|
|
14
|
+
app: string;
|
|
15
|
+
/** The app's version — the package's own CalVer, ordinarily. */
|
|
16
|
+
version: string;
|
|
17
|
+
/**
|
|
18
|
+
* The Faro collector endpoint. Empty means "no collector configured" and
|
|
19
|
+
* the whole module stays inert rather than shipping beacons that error.
|
|
20
|
+
*/
|
|
21
|
+
url: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Start browser telemetry. Returns the Faro instance, or null when there is
|
|
25
|
+
* no collector to send to — the caller has nothing to clean up in that case,
|
|
26
|
+
* because nothing was created.
|
|
27
|
+
*
|
|
28
|
+
* Idempotent: a second call returns the first instance and initialises
|
|
29
|
+
* nothing. A layout remount, a dev-mode HMR leg or two init calls racing in
|
|
30
|
+
* different modules must not produce a second Faro global (faro-core refuses
|
|
31
|
+
* a second registration outright, and the session id a second instance would
|
|
32
|
+
* hand out would not be the one the first instance's events carry).
|
|
33
|
+
*/
|
|
34
|
+
export declare function initBrowserTelemetry({ app, version, url }: BrowserTelemetryOptions): Faro | null;
|
|
35
|
+
/**
|
|
36
|
+
* Faro's current session id, or null when telemetry is not running (no URL, or
|
|
37
|
+
* init not yet called). The null leg is ordinary, not an error: the feedback
|
|
38
|
+
* widget's `session_id` field carries it as null and the report still sends.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getSessionId(): string | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser telemetry for a household frontend: one `initBrowserTelemetry()`
|
|
3
|
+
* call in the client entry, one Faro instance for the app's lifetime.
|
|
4
|
+
*
|
|
5
|
+
* Both faro packages are ordinary `dependencies`, not peers: telemetry is not
|
|
6
|
+
* an optional flavour a consumer chooses per route, it is part of what every
|
|
7
|
+
* household app mounts, and a duplicated faro global would strand the session
|
|
8
|
+
* id the feedback widget reports against.
|
|
9
|
+
*/
|
|
10
|
+
import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';
|
|
11
|
+
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
|
|
12
|
+
/** The one Faro instance this browser tab holds, once initialised. */
|
|
13
|
+
let faro = null;
|
|
14
|
+
/**
|
|
15
|
+
* Start browser telemetry. Returns the Faro instance, or null when there is
|
|
16
|
+
* no collector to send to — the caller has nothing to clean up in that case,
|
|
17
|
+
* because nothing was created.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: a second call returns the first instance and initialises
|
|
20
|
+
* nothing. A layout remount, a dev-mode HMR leg or two init calls racing in
|
|
21
|
+
* different modules must not produce a second Faro global (faro-core refuses
|
|
22
|
+
* a second registration outright, and the session id a second instance would
|
|
23
|
+
* hand out would not be the one the first instance's events carry).
|
|
24
|
+
*/
|
|
25
|
+
export function initBrowserTelemetry({ app, version, url }) {
|
|
26
|
+
// No URL, no telemetry. An app that was never told where the collector
|
|
27
|
+
// lives ships a silent no-op rather than a transport that fails on every
|
|
28
|
+
// send — the call sites never branch on this; `getSessionId()` reads the
|
|
29
|
+
// same null either way.
|
|
30
|
+
if (!url)
|
|
31
|
+
return null;
|
|
32
|
+
if (faro)
|
|
33
|
+
return faro;
|
|
34
|
+
faro = initializeFaro({
|
|
35
|
+
url,
|
|
36
|
+
app: { name: app, version },
|
|
37
|
+
// Session tracking is Faro's default too, but the feedback widget's
|
|
38
|
+
// payload carries the session id, so the guarantee is stated here
|
|
39
|
+
// rather than inherited from a default that could change upstream.
|
|
40
|
+
sessionTracking: { enabled: true },
|
|
41
|
+
instrumentations: [
|
|
42
|
+
...getWebInstrumentations({
|
|
43
|
+
// The console stays local by rule: a household app's logs are not
|
|
44
|
+
// shipped to the collector, only errors, events and measurements.
|
|
45
|
+
captureConsole: false
|
|
46
|
+
}),
|
|
47
|
+
// W3C trace context on same-origin fetch and XHR: every request the
|
|
48
|
+
// app's own backend serves carries a `traceparent`, so a feedback
|
|
49
|
+
// report can be correlated with the server-side trace of the page
|
|
50
|
+
// it was sent from.
|
|
51
|
+
new TracingInstrumentation()
|
|
52
|
+
]
|
|
53
|
+
});
|
|
54
|
+
return faro;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Faro's current session id, or null when telemetry is not running (no URL, or
|
|
58
|
+
* init not yet called). The null leg is ordinary, not an error: the feedback
|
|
59
|
+
* widget's `session_id` field carries it as null and the report still sends.
|
|
60
|
+
*/
|
|
61
|
+
export function getSessionId() {
|
|
62
|
+
return faro?.api.getSession()?.id ?? null;
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.7",
|
|
4
4
|
"description": "Household shared component layer: shadcn-svelte primitives (bits-ui) plus the composed page chrome every app builds its routes from, restyled by each app's @poodle64/design-tokens alias layer. One fix reaches every app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
"types": "./dist/format.d.ts",
|
|
30
30
|
"svelte": "./dist/format.js"
|
|
31
31
|
},
|
|
32
|
+
"./telemetry": {
|
|
33
|
+
"types": "./dist/telemetry.d.ts",
|
|
34
|
+
"svelte": "./dist/telemetry.js"
|
|
35
|
+
},
|
|
36
|
+
"./feedback": {
|
|
37
|
+
"types": "./dist/components/feedback/index.d.ts",
|
|
38
|
+
"svelte": "./dist/components/feedback/index.js"
|
|
39
|
+
},
|
|
32
40
|
"./*": {
|
|
33
41
|
"types": "./dist/components/ui/*/index.d.ts",
|
|
34
42
|
"svelte": "./dist/components/ui/*/index.js"
|
|
@@ -60,10 +68,13 @@
|
|
|
60
68
|
}
|
|
61
69
|
},
|
|
62
70
|
"dependencies": {
|
|
71
|
+
"@grafana/faro-web-sdk": "^2.11.0",
|
|
72
|
+
"@grafana/faro-web-tracing": "^2.11.0",
|
|
63
73
|
"@jsonforms/core": "^3.8.0",
|
|
64
74
|
"@lucide/svelte": "^1.25.0",
|
|
65
75
|
"@tanstack/table-core": "^8.21.3",
|
|
66
76
|
"clsx": "^2.1.1",
|
|
77
|
+
"html-to-image": "^1.11.13",
|
|
67
78
|
"tailwind-merge": "^3.6.0",
|
|
68
79
|
"tailwind-variants": "^3.2.2",
|
|
69
80
|
"tw-animate-css": "^1.4.0"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @poodle64/ui — situation → component map
|
|
2
2
|
|
|
3
3
|
<!-- GENERATED by scripts/generate-registry.mjs from scripts/situations.json + package source. DO NOT EDIT. -->
|
|
4
|
-
Generated from `@poodle64/ui@2026.9.
|
|
4
|
+
Generated from `@poodle64/ui@2026.9.7`. 56 components, 15 situations.
|
|
5
5
|
|
|
6
6
|
**Read this before writing a `<div>`.** Find the SITUATION you are in below, then compose the component named for it — do not hand-build it from raw `Card` or utility classes. Import is `import { Name } from '<import path>'`. Props marked `?` are optional. This map is the retrieval step the [`frontend-design` skill] makes mandatory; the [CHI 2026 study] measured composing-from-a-registry at 95% design-system compliance against 71% for writing the CSS from a prose style guide.
|
|
7
7
|
|