@myna-sh/react 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/dist/feedback/index.d.ts +157 -0
- package/dist/feedback/index.js +422 -0
- package/dist/feedback/index.js.map +1 -0
- package/dist/feedback.css +286 -0
- package/dist/index.js +1 -1
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# @myna-sh/react
|
|
2
2
|
|
|
3
|
-
React
|
|
3
|
+
React bindings for [Myna](https://myna.sh): content hooks, and the reporter's side of Myna Feedback.
|
|
4
4
|
|
|
5
|
-
A thin layer over [`@myna-sh/sdk`](https://www.npmjs.com/package/@myna-sh/sdk) with no dependencies of its own
|
|
5
|
+
A thin layer over [`@myna-sh/sdk`](https://www.npmjs.com/package/@myna-sh/sdk) with no dependencies of its own. Two entry points:
|
|
6
|
+
|
|
7
|
+
| Import | What it is |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `@myna-sh/react` | Content hooks: request sharing, stale-while-revalidate, preview propagation, Suspense |
|
|
10
|
+
| `@myna-sh/react/feedback` | Unstyled components and hooks for filing a bug report and following the answer |
|
|
6
11
|
|
|
7
12
|
## Install
|
|
8
13
|
|
|
@@ -80,6 +85,32 @@ Each returns `{ data, error, isLoading, isValidating, refetch }` and accepts the
|
|
|
80
85
|
|
|
81
86
|
If your app already uses TanStack Query or SWR, call `@myna-sh/sdk` from it directly rather than adding these hooks — two caches holding the same content will disagree.
|
|
82
87
|
|
|
88
|
+
## Feedback
|
|
89
|
+
|
|
90
|
+
`@myna-sh/react/feedback` renders the board's own intake form, the reports somebody has filed, and the thread where they read the answer and say whether it worked. It runs inside your product: Myna serves no reporter-facing page.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { FeedbackProvider, ReportForm, MyReports, ReportThread } from "@myna-sh/react/feedback";
|
|
94
|
+
import "@myna-sh/react/feedback.css"; // optional
|
|
95
|
+
|
|
96
|
+
<FeedbackProvider
|
|
97
|
+
ingestKey="myna_ik_…"
|
|
98
|
+
board="bugs"
|
|
99
|
+
identify={{ id: user.id, email: user.email, signature }}
|
|
100
|
+
context={() => ({ appVersion: BUILD.version, route: location.pathname })}
|
|
101
|
+
>
|
|
102
|
+
<ReportForm onSubmitted={(r) => setOpen(r.number)} />
|
|
103
|
+
<MyReports onSelect={setOpen} />
|
|
104
|
+
{open ? <ReportThread number={open} /> : null}
|
|
105
|
+
</FeedbackProvider>;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`signature` comes from your own server — `signIdentity` in `@myna-sh/sdk/feedback/server`. Without it a report can still be filed, but nothing can be read back: an unsigned claim about who somebody is would let any visitor read another person's reports.
|
|
109
|
+
|
|
110
|
+
Headless hooks — `useFeedback`, `useBoardSchema`, `useSubmitReport`, `useMyReports`, `useReport` — are exported for anyone rendering their own.
|
|
111
|
+
|
|
112
|
+
Every element is unstyled and carries a `data-myna` attribute. The optional stylesheet is a starting point driven by `--myna-accent`, `--myna-radius` and `--myna-font`; everything else inherits from the surface around it. See [React feedback components](https://docs.myna.sh/sdk/react-feedback).
|
|
113
|
+
|
|
83
114
|
## Links
|
|
84
115
|
|
|
85
116
|
- [Myna](https://myna.sh)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { IdentifyOptions, FeedbackOptions, FeedbackClient, SubmitInput, SubmitResult, BoardSchema, MyReportSummary, MyReport } from '@myna-sh/sdk/feedback';
|
|
3
|
+
export { BoardFormField, BoardSchema, FeedbackClient, FeedbackError, IdentifyOptions, MyReport, MyReportSummary, SubmitInput, SubmitResult } from '@myna-sh/sdk/feedback';
|
|
4
|
+
|
|
5
|
+
interface FeedbackProviderProps {
|
|
6
|
+
/** A publishable ingest key (`myna_ik_...`). Safe to ship in your bundle. */
|
|
7
|
+
ingestKey: string;
|
|
8
|
+
/** Override for self-hosted or staging deployments. */
|
|
9
|
+
apiUrl?: string;
|
|
10
|
+
/** Which board to file on. Required when the key is not bound to one. */
|
|
11
|
+
board?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Who is using your application, signed by your own backend.
|
|
14
|
+
*
|
|
15
|
+
* Pass it once you know, and pass `undefined` when they sign out. Without it
|
|
16
|
+
* a report can still be filed (unless the board requires identity) but
|
|
17
|
+
* nothing can be read back — an unsigned claim about who somebody is would
|
|
18
|
+
* otherwise let any visitor read another person's reports.
|
|
19
|
+
*/
|
|
20
|
+
identify?: IdentifyOptions;
|
|
21
|
+
/**
|
|
22
|
+
* Environment attached to every report — app version, commit, route.
|
|
23
|
+
* A function is re-read on each submission, so a single-page app can report
|
|
24
|
+
* the route the user is actually on.
|
|
25
|
+
*/
|
|
26
|
+
context?: FeedbackOptions["context"];
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Provide a feedback client to the components and hooks below.
|
|
31
|
+
*
|
|
32
|
+
* The client is memoized on the values that change how it talks to Myna, and
|
|
33
|
+
* **not** on `identify` — a signature is refreshed by your backend more often
|
|
34
|
+
* than a key changes, and rebuilding the client on each one would discard
|
|
35
|
+
* nothing useful while making every child re-render. The identity is pushed
|
|
36
|
+
* into the existing client instead, in an effect, so signing in mid-session
|
|
37
|
+
* takes effect without a remount.
|
|
38
|
+
*/
|
|
39
|
+
declare function FeedbackProvider({ ingestKey, apiUrl, board, identify, context, children, }: FeedbackProviderProps): ReactNode;
|
|
40
|
+
|
|
41
|
+
declare function useFeedback(): FeedbackClient;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Headless hooks over the feedback client.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately thinner than `useMyna*` on the content side. Content reads are
|
|
47
|
+
* shared, cached and revalidated because a page renders dozens of them; a
|
|
48
|
+
* feedback surface renders one form and one list, and a cache would mostly add
|
|
49
|
+
* a way for a reporter to see a stale answer to the question they just asked.
|
|
50
|
+
* So: a request, loading and error state, and an explicit `refresh`.
|
|
51
|
+
*/
|
|
52
|
+
interface AsyncResult<T> {
|
|
53
|
+
data: T | undefined;
|
|
54
|
+
error: Error | undefined;
|
|
55
|
+
isLoading: boolean;
|
|
56
|
+
refresh: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The board's intake form.
|
|
61
|
+
*
|
|
62
|
+
* Every question the board asks, in the order it asks them — including the ones
|
|
63
|
+
* that fill the report's own title, body and reply address. Render the list and
|
|
64
|
+
* you have rendered the whole form; there is no question you are expected to
|
|
65
|
+
* supply yourself.
|
|
66
|
+
*/
|
|
67
|
+
declare function useBoardSchema(board?: string): AsyncResult<BoardSchema>;
|
|
68
|
+
interface SubmitState {
|
|
69
|
+
submit: (input: SubmitInput) => Promise<SubmitResult>;
|
|
70
|
+
isSubmitting: boolean;
|
|
71
|
+
error: Error | undefined;
|
|
72
|
+
/** The last successful submission, so a form can say which number it filed. */
|
|
73
|
+
result: SubmitResult | undefined;
|
|
74
|
+
reset: () => void;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* File a report.
|
|
78
|
+
*
|
|
79
|
+
* `submit` resolves with the report number and rejects with the `FeedbackError`
|
|
80
|
+
* the API answered — its `fields` record is keyed by path, so validation
|
|
81
|
+
* messages bind straight back to your inputs.
|
|
82
|
+
*/
|
|
83
|
+
declare function useSubmitReport(): SubmitState;
|
|
84
|
+
/**
|
|
85
|
+
* The reports this identity has filed.
|
|
86
|
+
*
|
|
87
|
+
* Empty until `FeedbackProvider` has an `identify`, because there is no such
|
|
88
|
+
* thing as an anonymous reporter's report list — the request would have nobody
|
|
89
|
+
* to be about.
|
|
90
|
+
*/
|
|
91
|
+
declare function useMyReports(): AsyncResult<MyReportSummary[]>;
|
|
92
|
+
/** One of their reports, with the public conversation and the actions on it. */
|
|
93
|
+
declare function useReport(number: number | undefined): AsyncResult<MyReport> & {
|
|
94
|
+
reply: (message: string) => Promise<void>;
|
|
95
|
+
confirm: (message?: string) => Promise<void>;
|
|
96
|
+
reopen: (reason?: string) => Promise<void>;
|
|
97
|
+
isActing: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
interface ReportFormProps {
|
|
101
|
+
/** Which board to file on. Required when the ingest key is not bound to one. */
|
|
102
|
+
board?: string;
|
|
103
|
+
/** Called with the report number after a successful submission. */
|
|
104
|
+
onSubmitted?: (result: SubmitResult) => void;
|
|
105
|
+
/** Rendered instead of the form once something has been filed. */
|
|
106
|
+
success?: (result: SubmitResult) => ReactNode;
|
|
107
|
+
/** Text for the parts the board does not declare. */
|
|
108
|
+
labels?: Partial<Record<"submit" | "submitting" | "sent" | "loading" | "unavailable", string>>;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The board's own intake form.
|
|
112
|
+
*
|
|
113
|
+
* Every question comes from the board, in the order the board asks them,
|
|
114
|
+
* including the ones that produce the report's title, description and reply
|
|
115
|
+
* address. Nothing here is hardcoded, which is what stops this component and
|
|
116
|
+
* your own hand-written form from asking the same board two different things.
|
|
117
|
+
*
|
|
118
|
+
* The consequence is that the form arrives one round trip late, and this
|
|
119
|
+
* renders a status line until it does. A form that has to be corrected after it
|
|
120
|
+
* appears is worse than one that appears a moment later.
|
|
121
|
+
*/
|
|
122
|
+
declare function ReportForm({ board, onSubmitted, success, labels }: ReportFormProps): ReactNode;
|
|
123
|
+
interface MyReportsProps {
|
|
124
|
+
/** Rendered for each report. Without it, each row is a plain button. */
|
|
125
|
+
children?: (report: {
|
|
126
|
+
number: number;
|
|
127
|
+
title: string;
|
|
128
|
+
status: string;
|
|
129
|
+
awaitingYou: boolean;
|
|
130
|
+
}) => ReactNode;
|
|
131
|
+
onSelect?: (number: number) => void;
|
|
132
|
+
empty?: ReactNode;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Everything this reporter has filed.
|
|
136
|
+
*
|
|
137
|
+
* Needs a signed identity on the provider. Without one it renders the empty
|
|
138
|
+
* state rather than an error, because "you have not reported anything" is what
|
|
139
|
+
* a signed-out visitor should see — not a sentence about HMAC.
|
|
140
|
+
*/
|
|
141
|
+
declare function MyReports({ children, onSelect, empty }: MyReportsProps): ReactNode;
|
|
142
|
+
interface ReportThreadProps {
|
|
143
|
+
number: number;
|
|
144
|
+
/** Text for the parts of the thread the board does not declare. */
|
|
145
|
+
labels?: Partial<Record<"reply" | "sending" | "placeholder" | "worksNow" | "stillBroken" | "loading", string>>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* One report and its conversation, with the two buttons the product exists for.
|
|
149
|
+
*
|
|
150
|
+
* "It works now" and "Still broken" appear only while `awaitingYou` — the team
|
|
151
|
+
* has changed something and asked this person to check. Showing them all the
|
|
152
|
+
* time would invite a reporter to close a report nobody has looked at yet, and
|
|
153
|
+
* the confirmation would then mean nothing.
|
|
154
|
+
*/
|
|
155
|
+
declare function ReportThread({ number, labels }: ReportThreadProps): ReactNode;
|
|
156
|
+
|
|
157
|
+
export { type AsyncResult, FeedbackProvider, type FeedbackProviderProps, MyReports, type MyReportsProps, ReportForm, type ReportFormProps, ReportThread, type ReportThreadProps, type SubmitState, useBoardSchema, useFeedback, useMyReports, useReport, useSubmitReport };
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// src/feedback/provider.tsx
|
|
2
|
+
import { useEffect, useMemo } from "react";
|
|
3
|
+
import { createFeedback } from "@myna-sh/sdk/feedback";
|
|
4
|
+
|
|
5
|
+
// src/feedback/context.ts
|
|
6
|
+
import { createContext, useContext } from "react";
|
|
7
|
+
var FeedbackContext = createContext(null);
|
|
8
|
+
function useFeedback() {
|
|
9
|
+
const client = useContext(FeedbackContext);
|
|
10
|
+
if (!client) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'No Myna feedback client in context. Wrap this tree in <FeedbackProvider ingestKey="myna_ik_...">.'
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return client;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/feedback/provider.tsx
|
|
19
|
+
import { jsx } from "react/jsx-runtime";
|
|
20
|
+
function FeedbackProvider({
|
|
21
|
+
ingestKey,
|
|
22
|
+
apiUrl,
|
|
23
|
+
board,
|
|
24
|
+
identify,
|
|
25
|
+
context,
|
|
26
|
+
children
|
|
27
|
+
}) {
|
|
28
|
+
const client = useMemo(
|
|
29
|
+
() => createFeedback({ ingestKey, apiUrl, board, context, identify }),
|
|
30
|
+
// `context` is deliberately absent: it is usually an inline object or an
|
|
31
|
+
// arrow function, so including it would rebuild the client on every render.
|
|
32
|
+
// The client re-reads a function form on each submission anyway.
|
|
33
|
+
[ingestKey, apiUrl, board]
|
|
34
|
+
);
|
|
35
|
+
const identityKey = identify ? JSON.stringify(identify) : "";
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
client.identify(identify ?? null);
|
|
38
|
+
}, [client, identityKey]);
|
|
39
|
+
return /* @__PURE__ */ jsx(FeedbackContext.Provider, { value: client, children });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/feedback/hooks.ts
|
|
43
|
+
import { useCallback, useEffect as useEffect2, useRef, useState } from "react";
|
|
44
|
+
function useAsync(run, deps, enabled = true) {
|
|
45
|
+
const [data, setData] = useState();
|
|
46
|
+
const [error, setError] = useState();
|
|
47
|
+
const [isLoading, setLoading] = useState(enabled);
|
|
48
|
+
const [nonce, setNonce] = useState(0);
|
|
49
|
+
const live = useRef(true);
|
|
50
|
+
useEffect2(() => {
|
|
51
|
+
live.current = true;
|
|
52
|
+
return () => {
|
|
53
|
+
live.current = false;
|
|
54
|
+
};
|
|
55
|
+
}, []);
|
|
56
|
+
useEffect2(() => {
|
|
57
|
+
if (!enabled) {
|
|
58
|
+
setLoading(false);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let current = true;
|
|
62
|
+
setLoading(true);
|
|
63
|
+
run().then((value) => {
|
|
64
|
+
if (!current || !live.current) return;
|
|
65
|
+
setData(value);
|
|
66
|
+
setError(void 0);
|
|
67
|
+
}).catch((err) => {
|
|
68
|
+
if (!current || !live.current) return;
|
|
69
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
70
|
+
}).finally(() => {
|
|
71
|
+
if (current && live.current) setLoading(false);
|
|
72
|
+
});
|
|
73
|
+
return () => {
|
|
74
|
+
current = false;
|
|
75
|
+
};
|
|
76
|
+
}, [...deps, enabled, nonce]);
|
|
77
|
+
return { data, error, isLoading, refresh: useCallback(() => setNonce((n) => n + 1), []) };
|
|
78
|
+
}
|
|
79
|
+
function useBoardSchema(board) {
|
|
80
|
+
const client = useFeedback();
|
|
81
|
+
return useAsync(() => client.schema(board), [client, board]);
|
|
82
|
+
}
|
|
83
|
+
function useSubmitReport() {
|
|
84
|
+
const client = useFeedback();
|
|
85
|
+
const [isSubmitting, setSubmitting] = useState(false);
|
|
86
|
+
const [error, setError] = useState();
|
|
87
|
+
const [result, setResult] = useState();
|
|
88
|
+
const submit = useCallback(
|
|
89
|
+
async (input) => {
|
|
90
|
+
setSubmitting(true);
|
|
91
|
+
setError(void 0);
|
|
92
|
+
try {
|
|
93
|
+
const value = await client.submit(input);
|
|
94
|
+
setResult(value);
|
|
95
|
+
return value;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
98
|
+
setError(wrapped);
|
|
99
|
+
throw wrapped;
|
|
100
|
+
} finally {
|
|
101
|
+
setSubmitting(false);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
[client]
|
|
105
|
+
);
|
|
106
|
+
const reset = useCallback(() => {
|
|
107
|
+
setError(void 0);
|
|
108
|
+
setResult(void 0);
|
|
109
|
+
}, []);
|
|
110
|
+
return { submit, isSubmitting, error, result, reset };
|
|
111
|
+
}
|
|
112
|
+
function useMyReports() {
|
|
113
|
+
const client = useFeedback();
|
|
114
|
+
return useAsync(() => client.myReports(), [client]);
|
|
115
|
+
}
|
|
116
|
+
function useReport(number) {
|
|
117
|
+
const client = useFeedback();
|
|
118
|
+
const query = useAsync(
|
|
119
|
+
() => client.report(number),
|
|
120
|
+
[client, number],
|
|
121
|
+
number !== void 0
|
|
122
|
+
);
|
|
123
|
+
const [isActing, setActing] = useState(false);
|
|
124
|
+
const { refresh } = query;
|
|
125
|
+
const act = useCallback(
|
|
126
|
+
async (run) => {
|
|
127
|
+
if (number === void 0) return;
|
|
128
|
+
setActing(true);
|
|
129
|
+
try {
|
|
130
|
+
await run(client, number);
|
|
131
|
+
refresh();
|
|
132
|
+
} finally {
|
|
133
|
+
setActing(false);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
[client, number, refresh]
|
|
137
|
+
);
|
|
138
|
+
return {
|
|
139
|
+
...query,
|
|
140
|
+
isActing,
|
|
141
|
+
reply: (message) => act((c, n) => c.reply(n, message)),
|
|
142
|
+
confirm: (message) => act((c, n) => c.confirm(n, message)),
|
|
143
|
+
reopen: (reason) => act((c, n) => c.reopen(n, reason))
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/feedback/components.tsx
|
|
148
|
+
import { useMemo as useMemo2, useState as useState2 } from "react";
|
|
149
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
150
|
+
function when(iso) {
|
|
151
|
+
const date = new Date(iso);
|
|
152
|
+
return Number.isNaN(date.getTime()) ? iso : date.toLocaleString();
|
|
153
|
+
}
|
|
154
|
+
var EVENT_TEXT = {
|
|
155
|
+
created: "You reported this.",
|
|
156
|
+
status_changed: "We changed its status.",
|
|
157
|
+
retest_requested: "We think this is fixed \u2014 can you confirm?",
|
|
158
|
+
reopened: "Reopened.",
|
|
159
|
+
merged: "We linked this to another report of the same problem.",
|
|
160
|
+
link_added: "We linked a fix.",
|
|
161
|
+
attachment_added: "A file was attached."
|
|
162
|
+
};
|
|
163
|
+
function eventText(kind) {
|
|
164
|
+
return EVENT_TEXT[kind] ?? kind.replaceAll("_", " ");
|
|
165
|
+
}
|
|
166
|
+
function humanize(key) {
|
|
167
|
+
const words = key.replaceAll(/[_-]+/g, " ").trim();
|
|
168
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
169
|
+
}
|
|
170
|
+
function labelOf(field) {
|
|
171
|
+
return field.label ?? humanize(field.key);
|
|
172
|
+
}
|
|
173
|
+
function inputType(field) {
|
|
174
|
+
switch (field.type) {
|
|
175
|
+
case "number":
|
|
176
|
+
return "number";
|
|
177
|
+
case "boolean":
|
|
178
|
+
return "checkbox";
|
|
179
|
+
case "date":
|
|
180
|
+
return "date";
|
|
181
|
+
case "email":
|
|
182
|
+
return "email";
|
|
183
|
+
default:
|
|
184
|
+
return "text";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
var FORM_TEXT = {
|
|
188
|
+
submit: "Send report",
|
|
189
|
+
submitting: "Sending\u2026",
|
|
190
|
+
sent: "Thanks \u2014 your report was received.",
|
|
191
|
+
loading: "Loading the form\u2026",
|
|
192
|
+
unavailable: "This form could not be loaded. Try again shortly."
|
|
193
|
+
};
|
|
194
|
+
function ReportForm({ board, onSubmitted, success, labels }) {
|
|
195
|
+
const text = { ...FORM_TEXT, ...labels };
|
|
196
|
+
const schema = useBoardSchema(board);
|
|
197
|
+
const submission = useSubmitReport();
|
|
198
|
+
const [values, setValues] = useState2({});
|
|
199
|
+
const [files, setFiles] = useState2([]);
|
|
200
|
+
const fields = schema.data?.fields ?? [];
|
|
201
|
+
const fieldErrors = useMemo2(() => {
|
|
202
|
+
const error = submission.error;
|
|
203
|
+
return error?.fields ?? {};
|
|
204
|
+
}, [submission.error]);
|
|
205
|
+
if (schema.isLoading) {
|
|
206
|
+
return /* @__PURE__ */ jsx2("p", { "data-myna": "status", role: "status", children: text.loading });
|
|
207
|
+
}
|
|
208
|
+
if (schema.error || !schema.data) {
|
|
209
|
+
return /* @__PURE__ */ jsx2("p", { "data-myna": "status", "data-myna-state": "error", role: "status", children: text.unavailable });
|
|
210
|
+
}
|
|
211
|
+
if (submission.result) {
|
|
212
|
+
return /* @__PURE__ */ jsx2("div", { "data-myna": "sent", role: "status", children: success ? success(submission.result) : /* @__PURE__ */ jsx2("p", { children: text.sent }) });
|
|
213
|
+
}
|
|
214
|
+
const onSubmit = (event) => {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
let title = "";
|
|
217
|
+
let body;
|
|
218
|
+
let email;
|
|
219
|
+
const custom = {};
|
|
220
|
+
for (const field of fields) {
|
|
221
|
+
if (field.target === "attachments") continue;
|
|
222
|
+
const raw = values[field.key];
|
|
223
|
+
if (field.type === "boolean") {
|
|
224
|
+
custom[field.key] = raw === true;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const value = typeof raw === "string" ? raw.trim() : "";
|
|
228
|
+
if (field.target === "title") title = value;
|
|
229
|
+
else if (field.target === "body") body = value || void 0;
|
|
230
|
+
else if (field.target === "reporterEmail") email = value || void 0;
|
|
231
|
+
else if (value !== "") custom[field.key] = field.type === "number" ? Number(value) : value;
|
|
232
|
+
}
|
|
233
|
+
void submission.submit({
|
|
234
|
+
board,
|
|
235
|
+
title,
|
|
236
|
+
body,
|
|
237
|
+
reporter: email ? { email } : void 0,
|
|
238
|
+
fields: Object.keys(custom).length > 0 ? custom : void 0,
|
|
239
|
+
attachments: files.length > 0 ? files : void 0
|
|
240
|
+
}).then((result) => onSubmitted?.(result)).catch(() => void 0);
|
|
241
|
+
};
|
|
242
|
+
return /* @__PURE__ */ jsxs("form", { "data-myna": "form", onSubmit, noValidate: true, children: [
|
|
243
|
+
fields.map((field) => /* @__PURE__ */ jsx2(
|
|
244
|
+
Question,
|
|
245
|
+
{
|
|
246
|
+
field,
|
|
247
|
+
value: values[field.key],
|
|
248
|
+
error: fieldErrors[field.key] ?? fieldErrors[`fields.${field.key}`],
|
|
249
|
+
onChange: (v) => setValues((prev) => ({ ...prev, [field.key]: v })),
|
|
250
|
+
onFiles: setFiles
|
|
251
|
+
},
|
|
252
|
+
field.key
|
|
253
|
+
)),
|
|
254
|
+
submission.error && Object.keys(fieldErrors).length === 0 ? /* @__PURE__ */ jsx2("p", { "data-myna": "error", role: "alert", children: submission.error.message }) : null,
|
|
255
|
+
/* @__PURE__ */ jsx2("button", { "data-myna": "submit", type: "submit", disabled: submission.isSubmitting, children: submission.isSubmitting ? text.submitting : text.submit })
|
|
256
|
+
] });
|
|
257
|
+
}
|
|
258
|
+
function Question({
|
|
259
|
+
field,
|
|
260
|
+
value,
|
|
261
|
+
error,
|
|
262
|
+
onChange,
|
|
263
|
+
onFiles
|
|
264
|
+
}) {
|
|
265
|
+
const id = `myna-${field.key}`;
|
|
266
|
+
const label = labelOf(field);
|
|
267
|
+
const control = field.target === "attachments" ? /* @__PURE__ */ jsx2(
|
|
268
|
+
"input",
|
|
269
|
+
{
|
|
270
|
+
id,
|
|
271
|
+
"data-myna": "input",
|
|
272
|
+
type: "file",
|
|
273
|
+
multiple: field.max !== 1,
|
|
274
|
+
required: field.required,
|
|
275
|
+
onChange: (e) => onFiles(Array.from(e.target.files ?? []))
|
|
276
|
+
}
|
|
277
|
+
) : field.type === "markdown" ? /* @__PURE__ */ jsx2(
|
|
278
|
+
"textarea",
|
|
279
|
+
{
|
|
280
|
+
id,
|
|
281
|
+
"data-myna": "input",
|
|
282
|
+
rows: 4,
|
|
283
|
+
required: field.required,
|
|
284
|
+
placeholder: field.placeholder,
|
|
285
|
+
value: typeof value === "string" ? value : "",
|
|
286
|
+
onChange: (e) => onChange(e.target.value)
|
|
287
|
+
}
|
|
288
|
+
) : field.type === "boolean" ? /* @__PURE__ */ jsx2(
|
|
289
|
+
"input",
|
|
290
|
+
{
|
|
291
|
+
id,
|
|
292
|
+
"data-myna": "input",
|
|
293
|
+
type: "checkbox",
|
|
294
|
+
checked: value === true,
|
|
295
|
+
onChange: (e) => onChange(e.target.checked)
|
|
296
|
+
}
|
|
297
|
+
) : /* @__PURE__ */ jsx2(
|
|
298
|
+
"input",
|
|
299
|
+
{
|
|
300
|
+
id,
|
|
301
|
+
"data-myna": "input",
|
|
302
|
+
type: inputType(field),
|
|
303
|
+
required: field.required,
|
|
304
|
+
placeholder: field.placeholder,
|
|
305
|
+
value: typeof value === "string" ? value : "",
|
|
306
|
+
onChange: (e) => onChange(e.target.value)
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
return /* @__PURE__ */ jsxs("div", { "data-myna": "field", "data-myna-field": field.key, children: [
|
|
310
|
+
/* @__PURE__ */ jsx2("label", { "data-myna": "label", htmlFor: id, children: label }),
|
|
311
|
+
control,
|
|
312
|
+
field.help ? /* @__PURE__ */ jsx2("small", { "data-myna": "help", children: field.help }) : null,
|
|
313
|
+
error ? /* @__PURE__ */ jsx2("small", { "data-myna": "field-error", role: "alert", children: error }) : null
|
|
314
|
+
] });
|
|
315
|
+
}
|
|
316
|
+
function MyReports({ children, onSelect, empty }) {
|
|
317
|
+
const { data, error, isLoading } = useMyReports();
|
|
318
|
+
if (isLoading) return /* @__PURE__ */ jsx2("p", { "data-myna": "status", role: "status", children: "Loading\u2026" });
|
|
319
|
+
if (error || !data || data.length === 0) {
|
|
320
|
+
return /* @__PURE__ */ jsx2("div", { "data-myna": "empty", children: empty ?? /* @__PURE__ */ jsx2("p", { children: "No reports yet." }) });
|
|
321
|
+
}
|
|
322
|
+
return /* @__PURE__ */ jsx2("ul", { "data-myna": "my-reports", children: data.map((report) => /* @__PURE__ */ jsxs("li", { "data-myna": "my-report", "data-myna-status": report.status, children: [
|
|
323
|
+
/* @__PURE__ */ jsx2("button", { type: "button", "data-myna": "my-report-link", onClick: () => onSelect?.(report.number), children: children ? children(report) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
324
|
+
/* @__PURE__ */ jsxs("span", { "data-myna": "my-report-number", children: [
|
|
325
|
+
"#",
|
|
326
|
+
report.number
|
|
327
|
+
] }),
|
|
328
|
+
/* @__PURE__ */ jsx2("span", { "data-myna": "my-report-title", children: report.title }),
|
|
329
|
+
/* @__PURE__ */ jsx2("span", { "data-myna": "my-report-status", children: report.status.replaceAll("_", " ") })
|
|
330
|
+
] }) }),
|
|
331
|
+
report.awaitingYou ? /* @__PURE__ */ jsx2("span", { "data-myna": "awaiting-you", children: "Waiting on you" }) : null
|
|
332
|
+
] }, report.number)) });
|
|
333
|
+
}
|
|
334
|
+
var THREAD_TEXT = {
|
|
335
|
+
reply: "Send",
|
|
336
|
+
sending: "Sending\u2026",
|
|
337
|
+
placeholder: "Add a reply\u2026",
|
|
338
|
+
worksNow: "It works now",
|
|
339
|
+
stillBroken: "Still broken",
|
|
340
|
+
loading: "Loading\u2026"
|
|
341
|
+
};
|
|
342
|
+
function ReportThread({ number, labels }) {
|
|
343
|
+
const text = { ...THREAD_TEXT, ...labels };
|
|
344
|
+
const report = useReport(number);
|
|
345
|
+
const [draft, setDraft] = useState2("");
|
|
346
|
+
if (report.isLoading) return /* @__PURE__ */ jsx2("p", { "data-myna": "status", role: "status", children: text.loading });
|
|
347
|
+
if (report.error || !report.data) {
|
|
348
|
+
return /* @__PURE__ */ jsx2("p", { "data-myna": "status", "data-myna-state": "error", role: "status", children: report.error?.message ?? "That report is not available." });
|
|
349
|
+
}
|
|
350
|
+
const data = report.data;
|
|
351
|
+
const send = () => {
|
|
352
|
+
const message = draft.trim();
|
|
353
|
+
if (!message) return;
|
|
354
|
+
void report.reply(message).then(() => setDraft(""));
|
|
355
|
+
};
|
|
356
|
+
return /* @__PURE__ */ jsxs("article", { "data-myna": "thread", "data-myna-status": data.status, children: [
|
|
357
|
+
/* @__PURE__ */ jsxs("header", { "data-myna": "thread-header", children: [
|
|
358
|
+
/* @__PURE__ */ jsxs("span", { "data-myna": "thread-number", children: [
|
|
359
|
+
"#",
|
|
360
|
+
data.number
|
|
361
|
+
] }),
|
|
362
|
+
/* @__PURE__ */ jsx2("h2", { "data-myna": "thread-title", children: data.title }),
|
|
363
|
+
/* @__PURE__ */ jsx2("span", { "data-myna": "thread-status", children: data.status.replaceAll("_", " ") })
|
|
364
|
+
] }),
|
|
365
|
+
data.body ? /* @__PURE__ */ jsx2("p", { "data-myna": "thread-body", children: data.body }) : null,
|
|
366
|
+
/* @__PURE__ */ jsx2("ol", { "data-myna": "timeline", children: data.timeline.map((event, i) => /* @__PURE__ */ jsxs("li", { "data-myna": "event", "data-myna-author": event.author, "data-myna-kind": event.kind, children: [
|
|
367
|
+
event.body ? /* @__PURE__ */ jsx2("p", { "data-myna": "event-body", children: event.body }) : /* @__PURE__ */ jsx2("p", { "data-myna": "event-summary", children: eventText(event.kind) }),
|
|
368
|
+
/* @__PURE__ */ jsx2("time", { "data-myna": "event-time", dateTime: event.createdAt, children: when(event.createdAt) })
|
|
369
|
+
] }, i)) }),
|
|
370
|
+
data.awaitingYou ? /* @__PURE__ */ jsxs("div", { "data-myna": "retest", children: [
|
|
371
|
+
/* @__PURE__ */ jsx2(
|
|
372
|
+
"button",
|
|
373
|
+
{
|
|
374
|
+
type: "button",
|
|
375
|
+
"data-myna": "confirm",
|
|
376
|
+
disabled: report.isActing,
|
|
377
|
+
onClick: () => void report.confirm(draft.trim() || void 0),
|
|
378
|
+
children: text.worksNow
|
|
379
|
+
}
|
|
380
|
+
),
|
|
381
|
+
/* @__PURE__ */ jsx2(
|
|
382
|
+
"button",
|
|
383
|
+
{
|
|
384
|
+
type: "button",
|
|
385
|
+
"data-myna": "reopen",
|
|
386
|
+
disabled: report.isActing,
|
|
387
|
+
onClick: () => void report.reopen(draft.trim() || void 0),
|
|
388
|
+
children: text.stillBroken
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
] }) : null,
|
|
392
|
+
/* @__PURE__ */ jsxs("div", { "data-myna": "reply", children: [
|
|
393
|
+
/* @__PURE__ */ jsx2(
|
|
394
|
+
"textarea",
|
|
395
|
+
{
|
|
396
|
+
"data-myna": "reply-input",
|
|
397
|
+
rows: 3,
|
|
398
|
+
value: draft,
|
|
399
|
+
placeholder: text.placeholder,
|
|
400
|
+
onChange: (e) => setDraft(e.target.value)
|
|
401
|
+
}
|
|
402
|
+
),
|
|
403
|
+
/* @__PURE__ */ jsx2("button", { type: "button", "data-myna": "reply-submit", disabled: report.isActing || !draft.trim(), onClick: send, children: report.isActing ? text.sending : text.reply })
|
|
404
|
+
] })
|
|
405
|
+
] });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/feedback/index.ts
|
|
409
|
+
import { FeedbackError } from "@myna-sh/sdk/feedback";
|
|
410
|
+
export {
|
|
411
|
+
FeedbackError,
|
|
412
|
+
FeedbackProvider,
|
|
413
|
+
MyReports,
|
|
414
|
+
ReportForm,
|
|
415
|
+
ReportThread,
|
|
416
|
+
useBoardSchema,
|
|
417
|
+
useFeedback,
|
|
418
|
+
useMyReports,
|
|
419
|
+
useReport,
|
|
420
|
+
useSubmitReport
|
|
421
|
+
};
|
|
422
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/provider.tsx","../../src/feedback/context.ts","../../src/feedback/hooks.ts","../../src/feedback/components.tsx","../../src/feedback/index.ts"],"sourcesContent":["import { useEffect, useMemo, type ReactNode } from \"react\";\nimport { createFeedback, type FeedbackOptions, type IdentifyOptions } from \"@myna-sh/sdk/feedback\";\nimport { FeedbackContext } from \"./context.js\";\n\nexport interface FeedbackProviderProps {\n /** A publishable ingest key (`myna_ik_...`). Safe to ship in your bundle. */\n ingestKey: string;\n /** Override for self-hosted or staging deployments. */\n apiUrl?: string;\n /** Which board to file on. Required when the key is not bound to one. */\n board?: string;\n /**\n * Who is using your application, signed by your own backend.\n *\n * Pass it once you know, and pass `undefined` when they sign out. Without it\n * a report can still be filed (unless the board requires identity) but\n * nothing can be read back — an unsigned claim about who somebody is would\n * otherwise let any visitor read another person's reports.\n */\n identify?: IdentifyOptions;\n /**\n * Environment attached to every report — app version, commit, route.\n * A function is re-read on each submission, so a single-page app can report\n * the route the user is actually on.\n */\n context?: FeedbackOptions[\"context\"];\n children: ReactNode;\n}\n\n/**\n * Provide a feedback client to the components and hooks below.\n *\n * The client is memoized on the values that change how it talks to Myna, and\n * **not** on `identify` — a signature is refreshed by your backend more often\n * than a key changes, and rebuilding the client on each one would discard\n * nothing useful while making every child re-render. The identity is pushed\n * into the existing client instead, in an effect, so signing in mid-session\n * takes effect without a remount.\n */\nexport function FeedbackProvider({\n ingestKey,\n apiUrl,\n board,\n identify,\n context,\n children,\n}: FeedbackProviderProps): ReactNode {\n const client = useMemo(\n () => createFeedback({ ingestKey, apiUrl, board, context, identify }),\n // `context` is deliberately absent: it is usually an inline object or an\n // arrow function, so including it would rebuild the client on every render.\n // The client re-reads a function form on each submission anyway.\n [ingestKey, apiUrl, board],\n );\n\n // Serialized rather than spread across a dependency list, because\n // `properties` is a nested object and a shallow list would silently miss a\n // change inside it.\n const identityKey = identify ? JSON.stringify(identify) : \"\";\n useEffect(() => {\n // `?? null` rather than a conditional: signing out has to *clear* the\n // identity, or the next person on a shared machine reads the last one's\n // reports.\n client.identify(identify ?? null);\n }, [client, identityKey]);\n\n return <FeedbackContext.Provider value={client}>{children}</FeedbackContext.Provider>;\n}\n","import { createContext, useContext } from \"react\";\nimport type { FeedbackClient } from \"@myna-sh/sdk/feedback\";\n\n/**\n * The feedback client every hook and component below reads from.\n *\n * Context rather than a prop for the same reason the content client is: a form\n * in a modal, a \"my reports\" list in an account page, and a thread on a\n * different route all need the same key, the same board and the same identity,\n * and threading those through each component by hand is how one of them ends up\n * unauthenticated.\n */\nexport const FeedbackContext = createContext<FeedbackClient | null>(null);\n\nexport function useFeedback(): FeedbackClient {\n const client = useContext(FeedbackContext);\n if (!client) {\n throw new Error(\n \"No Myna feedback client in context. Wrap this tree in <FeedbackProvider ingestKey=\\\"myna_ik_...\\\">.\",\n );\n }\n return client;\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type {\n BoardSchema,\n FeedbackClient,\n MyReport,\n MyReportSummary,\n SubmitInput,\n SubmitResult,\n} from \"@myna-sh/sdk/feedback\";\nimport { useFeedback } from \"./context.js\";\n\n/**\n * Headless hooks over the feedback client.\n *\n * Deliberately thinner than `useMyna*` on the content side. Content reads are\n * shared, cached and revalidated because a page renders dozens of them; a\n * feedback surface renders one form and one list, and a cache would mostly add\n * a way for a reporter to see a stale answer to the question they just asked.\n * So: a request, loading and error state, and an explicit `refresh`.\n */\n\nexport interface AsyncResult<T> {\n data: T | undefined;\n error: Error | undefined;\n isLoading: boolean;\n refresh: () => void;\n}\n\n/** One request, cancelled on unmount, re-run when `deps` change or on refresh. */\nfunction useAsync<T>(run: () => Promise<T>, deps: unknown[], enabled = true): AsyncResult<T> {\n const [data, setData] = useState<T>();\n const [error, setError] = useState<Error>();\n const [isLoading, setLoading] = useState(enabled);\n const [nonce, setNonce] = useState(0);\n // A guard rather than an AbortController: the client's methods do not take a\n // signal, and what actually matters is not writing state after unmount.\n const live = useRef(true);\n\n useEffect(() => {\n live.current = true;\n return () => {\n live.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (!enabled) {\n setLoading(false);\n return;\n }\n let current = true;\n setLoading(true);\n run()\n .then((value) => {\n if (!current || !live.current) return;\n setData(value);\n setError(undefined);\n })\n .catch((err: unknown) => {\n if (!current || !live.current) return;\n setError(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n if (current && live.current) setLoading(false);\n });\n return () => {\n current = false;\n };\n }, [...deps, enabled, nonce]);\n\n return { data, error, isLoading, refresh: useCallback(() => setNonce((n) => n + 1), []) };\n}\n\nexport { useFeedback };\n\n/**\n * The board's intake form.\n *\n * Every question the board asks, in the order it asks them — including the ones\n * that fill the report's own title, body and reply address. Render the list and\n * you have rendered the whole form; there is no question you are expected to\n * supply yourself.\n */\nexport function useBoardSchema(board?: string): AsyncResult<BoardSchema> {\n const client = useFeedback();\n return useAsync(() => client.schema(board), [client, board]);\n}\n\nexport interface SubmitState {\n submit: (input: SubmitInput) => Promise<SubmitResult>;\n isSubmitting: boolean;\n error: Error | undefined;\n /** The last successful submission, so a form can say which number it filed. */\n result: SubmitResult | undefined;\n reset: () => void;\n}\n\n/**\n * File a report.\n *\n * `submit` resolves with the report number and rejects with the `FeedbackError`\n * the API answered — its `fields` record is keyed by path, so validation\n * messages bind straight back to your inputs.\n */\nexport function useSubmitReport(): SubmitState {\n const client = useFeedback();\n const [isSubmitting, setSubmitting] = useState(false);\n const [error, setError] = useState<Error>();\n const [result, setResult] = useState<SubmitResult>();\n\n const submit = useCallback(\n async (input: SubmitInput) => {\n setSubmitting(true);\n setError(undefined);\n try {\n const value = await client.submit(input);\n setResult(value);\n return value;\n } catch (err) {\n const wrapped = err instanceof Error ? err : new Error(String(err));\n setError(wrapped);\n throw wrapped;\n } finally {\n setSubmitting(false);\n }\n },\n [client],\n );\n\n const reset = useCallback(() => {\n setError(undefined);\n setResult(undefined);\n }, []);\n\n return { submit, isSubmitting, error, result, reset };\n}\n\n/**\n * The reports this identity has filed.\n *\n * Empty until `FeedbackProvider` has an `identify`, because there is no such\n * thing as an anonymous reporter's report list — the request would have nobody\n * to be about.\n */\nexport function useMyReports(): AsyncResult<MyReportSummary[]> {\n const client = useFeedback();\n return useAsync(() => client.myReports(), [client]);\n}\n\n/** One of their reports, with the public conversation and the actions on it. */\nexport function useReport(number: number | undefined): AsyncResult<MyReport> & {\n reply: (message: string) => Promise<void>;\n confirm: (message?: string) => Promise<void>;\n reopen: (reason?: string) => Promise<void>;\n isActing: boolean;\n} {\n const client = useFeedback();\n const query = useAsync(\n () => client.report(number!),\n [client, number],\n number !== undefined,\n );\n const [isActing, setActing] = useState(false);\n // `query.refresh` rather than `query`: the result object is new on every\n // render, so depending on it would rebuild every action callback each time.\n const { refresh } = query;\n\n // Each action refetches, so the pane repaints from the server's answer rather\n // than from a guess about what the action did.\n const act = useCallback(\n async (run: (c: FeedbackClient, n: number) => Promise<MyReport>) => {\n if (number === undefined) return;\n setActing(true);\n try {\n await run(client, number);\n refresh();\n } finally {\n setActing(false);\n }\n },\n [client, number, refresh],\n );\n\n return {\n ...query,\n isActing,\n reply: (message) => act((c, n) => c.reply(n, message)),\n confirm: (message) => act((c, n) => c.confirm(n, message)),\n reopen: (reason) => act((c, n) => c.reopen(n, reason)),\n };\n}\n","import { useMemo, useState, type FormEvent, type ReactNode } from \"react\";\nimport type { BoardFormField, MyReport, SubmitResult } from \"@myna-sh/sdk/feedback\";\nimport { useBoardSchema, useMyReports, useReport, useSubmitReport } from \"./hooks.js\";\n\n/**\n * Components for the reporter's side of Feedback.\n *\n * **Unstyled.** Every element carries a `data-myna` attribute and nothing else —\n * no class names, no inline styles, no CSS-in-JS, no framework. Style them with\n * your own CSS, or import `@myna-sh/react/feedback.css` for a plain starting\n * point driven by three custom properties.\n *\n * That is not minimalism for its own sake. A feedback form sits inside somebody\n * else's product, next to their buttons and their type; a component that\n * arrives with opinions is a component that has to be fought, and every widget\n * that ships a stylesheet ends up in a specificity war with the application\n * embedding it.\n */\n\n/**\n * A timestamp a person can read, in their own locale.\n *\n * The machine-readable value stays on the `<time dateTime>` attribute, so\n * nothing is lost — but a raw ISO string is not something to show the person\n * who reported a bug, and asking every embedder to reformat it would make the\n * first thing they do to these components a fix.\n */\nfunction when(iso: string): string {\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? iso : date.toLocaleString();\n}\n\n/**\n * What a timeline entry that carries no prose means.\n *\n * A report's timeline is the conversation *and* the state record, so a reporter\n * sees rows with no body — the report being filed, a retest being asked for.\n * Without a sentence each of those renders as a bare timestamp, which reads as\n * something failing to load.\n *\n * Worded from the reporter's side, and deliberately vague about who did it: the\n * team is \"we\", never a name. An unrecognised kind falls back to its own words\n * rather than disappearing, so a kind added later degrades to readable.\n */\nconst EVENT_TEXT: Record<string, string> = {\n created: \"You reported this.\",\n status_changed: \"We changed its status.\",\n retest_requested: \"We think this is fixed — can you confirm?\",\n reopened: \"Reopened.\",\n merged: \"We linked this to another report of the same problem.\",\n link_added: \"We linked a fix.\",\n attachment_added: \"A file was attached.\",\n};\n\nfunction eventText(kind: string): string {\n return EVENT_TEXT[kind] ?? kind.replaceAll(\"_\", \" \");\n}\n\n/** `browser_version` -> `Browser version`, for a field that declared no label. */\nfunction humanize(key: string): string {\n const words = key.replaceAll(/[_-]+/g, \" \").trim();\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\nfunction labelOf(field: BoardFormField): string {\n return field.label ?? humanize(field.key);\n}\n\n/** The input type one declared question renders as. */\nfunction inputType(field: BoardFormField): string {\n switch (field.type) {\n case \"number\":\n return \"number\";\n case \"boolean\":\n return \"checkbox\";\n case \"date\":\n return \"date\";\n case \"email\":\n return \"email\";\n default:\n return \"text\";\n }\n}\n\nexport interface ReportFormProps {\n /** Which board to file on. Required when the ingest key is not bound to one. */\n board?: string;\n /** Called with the report number after a successful submission. */\n onSubmitted?: (result: SubmitResult) => void;\n /** Rendered instead of the form once something has been filed. */\n success?: (result: SubmitResult) => ReactNode;\n /** Text for the parts the board does not declare. */\n labels?: Partial<Record<\"submit\" | \"submitting\" | \"sent\" | \"loading\" | \"unavailable\", string>>;\n}\n\nconst FORM_TEXT = {\n submit: \"Send report\",\n submitting: \"Sending…\",\n sent: \"Thanks — your report was received.\",\n loading: \"Loading the form…\",\n unavailable: \"This form could not be loaded. Try again shortly.\",\n};\n\n/**\n * The board's own intake form.\n *\n * Every question comes from the board, in the order the board asks them,\n * including the ones that produce the report's title, description and reply\n * address. Nothing here is hardcoded, which is what stops this component and\n * your own hand-written form from asking the same board two different things.\n *\n * The consequence is that the form arrives one round trip late, and this\n * renders a status line until it does. A form that has to be corrected after it\n * appears is worse than one that appears a moment later.\n */\nexport function ReportForm({ board, onSubmitted, success, labels }: ReportFormProps): ReactNode {\n const text = { ...FORM_TEXT, ...labels };\n const schema = useBoardSchema(board);\n const submission = useSubmitReport();\n const [values, setValues] = useState<Record<string, string | boolean>>({});\n const [files, setFiles] = useState<File[]>([]);\n\n const fields = schema.data?.fields ?? [];\n const fieldErrors = useMemo(() => {\n const error = submission.error as { fields?: Record<string, string> } | undefined;\n return error?.fields ?? {};\n }, [submission.error]);\n\n if (schema.isLoading) {\n return (\n <p data-myna=\"status\" role=\"status\">\n {text.loading}\n </p>\n );\n }\n if (schema.error || !schema.data) {\n return (\n <p data-myna=\"status\" data-myna-state=\"error\" role=\"status\">\n {text.unavailable}\n </p>\n );\n }\n if (submission.result) {\n return (\n <div data-myna=\"sent\" role=\"status\">\n {success ? success(submission.result) : <p>{text.sent}</p>}\n </div>\n );\n }\n\n const onSubmit = (event: FormEvent) => {\n event.preventDefault();\n // The board says which answer fills which part of the report; everything it\n // does not target is a custom field.\n let title = \"\";\n let body: string | undefined;\n let email: string | undefined;\n const custom: Record<string, unknown> = {};\n\n for (const field of fields) {\n if (field.target === \"attachments\") continue;\n const raw = values[field.key];\n if (field.type === \"boolean\") {\n custom[field.key] = raw === true;\n continue;\n }\n const value = typeof raw === \"string\" ? raw.trim() : \"\";\n if (field.target === \"title\") title = value;\n else if (field.target === \"body\") body = value || undefined;\n else if (field.target === \"reporterEmail\") email = value || undefined;\n else if (value !== \"\") custom[field.key] = field.type === \"number\" ? Number(value) : value;\n }\n\n void submission\n .submit({\n board,\n title,\n body,\n reporter: email ? { email } : undefined,\n fields: Object.keys(custom).length > 0 ? custom : undefined,\n attachments: files.length > 0 ? files : undefined,\n })\n .then((result) => onSubmitted?.(result))\n // The hook already holds the error and the form renders it; rethrowing\n // here would surface an unhandled rejection for a state a user can see.\n .catch(() => undefined);\n };\n\n return (\n <form data-myna=\"form\" onSubmit={onSubmit} noValidate>\n {fields.map((field) => (\n <Question\n key={field.key}\n field={field}\n value={values[field.key]}\n error={fieldErrors[field.key] ?? fieldErrors[`fields.${field.key}`]}\n onChange={(v) => setValues((prev) => ({ ...prev, [field.key]: v }))}\n onFiles={setFiles}\n />\n ))}\n\n {submission.error && Object.keys(fieldErrors).length === 0 ? (\n <p data-myna=\"error\" role=\"alert\">\n {submission.error.message}\n </p>\n ) : null}\n\n <button data-myna=\"submit\" type=\"submit\" disabled={submission.isSubmitting}>\n {submission.isSubmitting ? text.submitting : text.submit}\n </button>\n </form>\n );\n}\n\nfunction Question({\n field,\n value,\n error,\n onChange,\n onFiles,\n}: {\n field: BoardFormField;\n value: string | boolean | undefined;\n error: string | undefined;\n onChange: (value: string | boolean) => void;\n onFiles: (files: File[]) => void;\n}): ReactNode {\n const id = `myna-${field.key}`;\n const label = labelOf(field);\n\n // The board asks for files by declaring a question. A board that does not ask\n // renders no file input, and the upload endpoint refuses one anyway.\n const control =\n field.target === \"attachments\" ? (\n <input\n id={id}\n data-myna=\"input\"\n type=\"file\"\n multiple={field.max !== 1}\n required={field.required}\n onChange={(e) => onFiles(Array.from(e.target.files ?? []))}\n />\n ) : field.type === \"markdown\" ? (\n <textarea\n id={id}\n data-myna=\"input\"\n rows={4}\n required={field.required}\n placeholder={field.placeholder}\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onChange(e.target.value)}\n />\n ) : field.type === \"boolean\" ? (\n <input\n id={id}\n data-myna=\"input\"\n type=\"checkbox\"\n checked={value === true}\n onChange={(e) => onChange(e.target.checked)}\n />\n ) : (\n <input\n id={id}\n data-myna=\"input\"\n type={inputType(field)}\n required={field.required}\n placeholder={field.placeholder}\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onChange(e.target.value)}\n />\n );\n\n return (\n <div data-myna=\"field\" data-myna-field={field.key}>\n <label data-myna=\"label\" htmlFor={id}>\n {label}\n </label>\n {control}\n {field.help ? <small data-myna=\"help\">{field.help}</small> : null}\n {error ? (\n <small data-myna=\"field-error\" role=\"alert\">\n {error}\n </small>\n ) : null}\n </div>\n );\n}\n\nexport interface MyReportsProps {\n /** Rendered for each report. Without it, each row is a plain button. */\n children?: (report: { number: number; title: string; status: string; awaitingYou: boolean }) => ReactNode;\n onSelect?: (number: number) => void;\n empty?: ReactNode;\n}\n\n/**\n * Everything this reporter has filed.\n *\n * Needs a signed identity on the provider. Without one it renders the empty\n * state rather than an error, because \"you have not reported anything\" is what\n * a signed-out visitor should see — not a sentence about HMAC.\n */\nexport function MyReports({ children, onSelect, empty }: MyReportsProps): ReactNode {\n const { data, error, isLoading } = useMyReports();\n\n if (isLoading) return <p data-myna=\"status\" role=\"status\">Loading…</p>;\n if (error || !data || data.length === 0) {\n return <div data-myna=\"empty\">{empty ?? <p>No reports yet.</p>}</div>;\n }\n\n return (\n <ul data-myna=\"my-reports\">\n {data.map((report) => (\n <li key={report.number} data-myna=\"my-report\" data-myna-status={report.status}>\n <button type=\"button\" data-myna=\"my-report-link\" onClick={() => onSelect?.(report.number)}>\n {children ? (\n children(report)\n ) : (\n <>\n <span data-myna=\"my-report-number\">#{report.number}</span>\n <span data-myna=\"my-report-title\">{report.title}</span>\n <span data-myna=\"my-report-status\">{report.status.replaceAll(\"_\", \" \")}</span>\n </>\n )}\n </button>\n {report.awaitingYou ? <span data-myna=\"awaiting-you\">Waiting on you</span> : null}\n </li>\n ))}\n </ul>\n );\n}\n\nexport interface ReportThreadProps {\n number: number;\n /** Text for the parts of the thread the board does not declare. */\n labels?: Partial<\n Record<\"reply\" | \"sending\" | \"placeholder\" | \"worksNow\" | \"stillBroken\" | \"loading\", string>\n >;\n}\n\nconst THREAD_TEXT = {\n reply: \"Send\",\n sending: \"Sending…\",\n placeholder: \"Add a reply…\",\n worksNow: \"It works now\",\n stillBroken: \"Still broken\",\n loading: \"Loading…\",\n};\n\n/**\n * One report and its conversation, with the two buttons the product exists for.\n *\n * \"It works now\" and \"Still broken\" appear only while `awaitingYou` — the team\n * has changed something and asked this person to check. Showing them all the\n * time would invite a reporter to close a report nobody has looked at yet, and\n * the confirmation would then mean nothing.\n */\nexport function ReportThread({ number, labels }: ReportThreadProps): ReactNode {\n const text = { ...THREAD_TEXT, ...labels };\n const report = useReport(number);\n const [draft, setDraft] = useState(\"\");\n\n if (report.isLoading) return <p data-myna=\"status\" role=\"status\">{text.loading}</p>;\n if (report.error || !report.data) {\n return (\n <p data-myna=\"status\" data-myna-state=\"error\" role=\"status\">\n {report.error?.message ?? \"That report is not available.\"}\n </p>\n );\n }\n\n const data: MyReport = report.data;\n const send = () => {\n const message = draft.trim();\n if (!message) return;\n void report.reply(message).then(() => setDraft(\"\"));\n };\n\n return (\n <article data-myna=\"thread\" data-myna-status={data.status}>\n <header data-myna=\"thread-header\">\n <span data-myna=\"thread-number\">#{data.number}</span>\n <h2 data-myna=\"thread-title\">{data.title}</h2>\n <span data-myna=\"thread-status\">{data.status.replaceAll(\"_\", \" \")}</span>\n </header>\n\n {data.body ? <p data-myna=\"thread-body\">{data.body}</p> : null}\n\n <ol data-myna=\"timeline\">\n {data.timeline.map((event, i) => (\n <li key={i} data-myna=\"event\" data-myna-author={event.author} data-myna-kind={event.kind}>\n {event.body ? (\n <p data-myna=\"event-body\">{event.body}</p>\n ) : (\n <p data-myna=\"event-summary\">{eventText(event.kind)}</p>\n )}\n <time data-myna=\"event-time\" dateTime={event.createdAt}>\n {when(event.createdAt)}\n </time>\n </li>\n ))}\n </ol>\n\n {data.awaitingYou ? (\n <div data-myna=\"retest\">\n <button\n type=\"button\"\n data-myna=\"confirm\"\n disabled={report.isActing}\n onClick={() => void report.confirm(draft.trim() || undefined)}\n >\n {text.worksNow}\n </button>\n <button\n type=\"button\"\n data-myna=\"reopen\"\n disabled={report.isActing}\n onClick={() => void report.reopen(draft.trim() || undefined)}\n >\n {text.stillBroken}\n </button>\n </div>\n ) : null}\n\n <div data-myna=\"reply\">\n <textarea\n data-myna=\"reply-input\"\n rows={3}\n value={draft}\n placeholder={text.placeholder}\n onChange={(e) => setDraft(e.target.value)}\n />\n <button type=\"button\" data-myna=\"reply-submit\" disabled={report.isActing || !draft.trim()} onClick={send}>\n {report.isActing ? text.sending : text.reply}\n </button>\n </div>\n </article>\n );\n}\n","/**\n * `@myna-sh/react/feedback` — file and follow bug reports from a React app.\n *\n * The reporter's side of Myna Feedback, as components and headless hooks over\n * `@myna-sh/sdk/feedback`. It runs inside **your** product, which is the point:\n * Myna serves no reporter-facing page, so the person who filed a bug reads the\n * answer where they already are and where they are already signed in.\n *\n * ```tsx\n * <FeedbackProvider ingestKey=\"myna_ik_...\" identify={{ id: user.id, signature }}>\n * <ReportForm board=\"bugs\" />\n * <MyReports onSelect={setOpen} />\n * {open ? <ReportThread number={open} /> : null}\n * </FeedbackProvider>\n * ```\n *\n * `signature` comes from your own backend — `signIdentity` in\n * `@myna-sh/sdk/feedback/server`. Without it a report can still be filed, but\n * nothing can be read back: an unsigned claim about who somebody is would let\n * any visitor read another person's reports.\n *\n * Every element is unstyled and carries a `data-myna` attribute. Bring your own\n * CSS, or import `@myna-sh/react/feedback.css` for a starting point driven by\n * `--myna-accent`, `--myna-radius` and `--myna-font`.\n */\nexport { FeedbackProvider, type FeedbackProviderProps } from \"./provider.js\";\nexport {\n useFeedback,\n useBoardSchema,\n useSubmitReport,\n useMyReports,\n useReport,\n type AsyncResult,\n type SubmitState,\n} from \"./hooks.js\";\nexport {\n ReportForm,\n MyReports,\n ReportThread,\n type ReportFormProps,\n type MyReportsProps,\n type ReportThreadProps,\n} from \"./components.js\";\nexport type {\n BoardFormField,\n BoardSchema,\n FeedbackClient,\n IdentifyOptions,\n MyReport,\n MyReportSummary,\n SubmitInput,\n SubmitResult,\n} from \"@myna-sh/sdk/feedback\";\nexport { FeedbackError } from \"@myna-sh/sdk/feedback\";\n"],"mappings":"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","names":["useEffect","useEffect","useMemo","useState","jsx","useState","useMemo"]}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `@myna-sh/react/feedback.css` — an optional starting point.
|
|
3
|
+
*
|
|
4
|
+
* Not a theme and not a design system: enough rules that the form looks
|
|
5
|
+
* deliberate before anyone has written CSS for it, and few enough that
|
|
6
|
+
* overriding one does not mean fighting five others. Everything selects on the
|
|
7
|
+
* `data-myna` attributes the components emit, so a plain class of your own wins
|
|
8
|
+
* on specificity without `!important`.
|
|
9
|
+
*
|
|
10
|
+
* Three custom properties, because those are the three things every embedding
|
|
11
|
+
* application actually wants to change:
|
|
12
|
+
*
|
|
13
|
+
* --myna-accent the button and focus colour (default: currentColor)
|
|
14
|
+
* --myna-radius corner radius on inputs and buttons (default: 6px)
|
|
15
|
+
* --myna-font the type stack (default: inherit)
|
|
16
|
+
*
|
|
17
|
+
* Set them on any ancestor. Colours otherwise come from `currentColor` and
|
|
18
|
+
* `color-mix`, so the form inherits the surrounding light or dark surface
|
|
19
|
+
* instead of asserting one.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
[data-myna="form"],
|
|
23
|
+
[data-myna="thread"],
|
|
24
|
+
[data-myna="my-reports"] {
|
|
25
|
+
--_accent: var(--myna-accent, currentColor);
|
|
26
|
+
--_radius: var(--myna-radius, 6px);
|
|
27
|
+
--_line: color-mix(in srgb, currentColor 22%, transparent);
|
|
28
|
+
--_muted: color-mix(in srgb, currentColor 62%, transparent);
|
|
29
|
+
font-family: var(--myna-font, inherit);
|
|
30
|
+
font-size: 14px;
|
|
31
|
+
line-height: 1.5;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* --- Form ---------------------------------------------------------------- */
|
|
35
|
+
|
|
36
|
+
[data-myna="form"] {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
gap: 14px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[data-myna="field"] {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
gap: 5px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[data-myna="label"] {
|
|
49
|
+
font-weight: 500;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[data-myna="input"] {
|
|
53
|
+
font: inherit;
|
|
54
|
+
color: inherit;
|
|
55
|
+
padding: 8px 10px;
|
|
56
|
+
border: 1px solid var(--_line);
|
|
57
|
+
border-radius: var(--_radius);
|
|
58
|
+
background: transparent;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[data-myna="input"]:focus-visible {
|
|
62
|
+
outline: 2px solid var(--_accent);
|
|
63
|
+
outline-offset: 1px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
[data-myna="input"][type="checkbox"] {
|
|
67
|
+
width: 1em;
|
|
68
|
+
height: 1em;
|
|
69
|
+
padding: 0;
|
|
70
|
+
align-self: start;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[data-myna="input"][type="file"] {
|
|
74
|
+
padding: 6px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
textarea[data-myna="input"],
|
|
78
|
+
[data-myna="reply-input"] {
|
|
79
|
+
resize: vertical;
|
|
80
|
+
min-height: 4.5em;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[data-myna="help"] {
|
|
84
|
+
color: var(--_muted);
|
|
85
|
+
font-size: 12px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[data-myna="field-error"],
|
|
89
|
+
[data-myna="error"] {
|
|
90
|
+
/* Red is the one colour worth asserting: an error the reader misses is the
|
|
91
|
+
failure this stylesheet exists to avoid. */
|
|
92
|
+
color: #c2410c;
|
|
93
|
+
font-size: 12px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[data-myna="submit"],
|
|
97
|
+
[data-myna="reply-submit"],
|
|
98
|
+
[data-myna="confirm"],
|
|
99
|
+
[data-myna="reopen"] {
|
|
100
|
+
font: inherit;
|
|
101
|
+
font-weight: 500;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
padding: 8px 14px;
|
|
104
|
+
border-radius: var(--_radius);
|
|
105
|
+
border: 1px solid var(--_line);
|
|
106
|
+
background: transparent;
|
|
107
|
+
color: inherit;
|
|
108
|
+
align-self: flex-start;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
* The primary buttons are tinted, not filled.
|
|
113
|
+
*
|
|
114
|
+
* A filled button needs a text colour that contrasts with the fill, and this
|
|
115
|
+
* stylesheet does not know what the fill is: `--myna-accent` defaults to
|
|
116
|
+
* `currentColor`, so filling with it and writing on top produced white on
|
|
117
|
+
* white. Tinting keeps the label at `currentColor` — readable on whatever
|
|
118
|
+
* surface the host page has — and still reads as the primary action once an
|
|
119
|
+
* accent is set.
|
|
120
|
+
*/
|
|
121
|
+
[data-myna="submit"],
|
|
122
|
+
[data-myna="confirm"] {
|
|
123
|
+
border-color: color-mix(in srgb, var(--_accent) 45%, transparent);
|
|
124
|
+
background: color-mix(in srgb, var(--_accent) 14%, transparent);
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
[data-myna="submit"]:disabled,
|
|
129
|
+
[data-myna="reply-submit"]:disabled,
|
|
130
|
+
[data-myna="confirm"]:disabled,
|
|
131
|
+
[data-myna="reopen"]:disabled {
|
|
132
|
+
opacity: 0.55;
|
|
133
|
+
cursor: default;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
[data-myna="status"],
|
|
137
|
+
[data-myna="empty"] {
|
|
138
|
+
color: var(--_muted);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
[data-myna="status"][data-myna-state="error"] {
|
|
142
|
+
color: #c2410c;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* --- The reporter's own reports ------------------------------------------ */
|
|
146
|
+
|
|
147
|
+
[data-myna="my-reports"] {
|
|
148
|
+
list-style: none;
|
|
149
|
+
margin: 0;
|
|
150
|
+
padding: 0;
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
gap: 2px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
[data-myna="my-report"] {
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 8px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
[data-myna="my-report-link"] {
|
|
163
|
+
font: inherit;
|
|
164
|
+
color: inherit;
|
|
165
|
+
cursor: pointer;
|
|
166
|
+
display: flex;
|
|
167
|
+
gap: 10px;
|
|
168
|
+
align-items: baseline;
|
|
169
|
+
flex: 1;
|
|
170
|
+
text-align: left;
|
|
171
|
+
padding: 9px 10px;
|
|
172
|
+
border: 0;
|
|
173
|
+
border-radius: var(--_radius);
|
|
174
|
+
background: transparent;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
[data-myna="my-report-link"]:hover {
|
|
178
|
+
background: color-mix(in srgb, currentColor 7%, transparent);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
[data-myna="my-report-number"],
|
|
182
|
+
[data-myna="thread-number"] {
|
|
183
|
+
font-variant-numeric: tabular-nums;
|
|
184
|
+
color: var(--_muted);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
[data-myna="my-report-title"] {
|
|
188
|
+
flex: 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
[data-myna="my-report-status"],
|
|
192
|
+
[data-myna="thread-status"],
|
|
193
|
+
[data-myna="awaiting-you"] {
|
|
194
|
+
font-size: 12px;
|
|
195
|
+
color: var(--_muted);
|
|
196
|
+
text-transform: lowercase;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Weight as well as colour: the accent may be `currentColor`, and "the team is
|
|
200
|
+
waiting on you" is the one label in the list that has to be noticed. */
|
|
201
|
+
[data-myna="awaiting-you"] {
|
|
202
|
+
color: var(--_accent);
|
|
203
|
+
font-weight: 600;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* --- One report ---------------------------------------------------------- */
|
|
207
|
+
|
|
208
|
+
[data-myna="thread"] {
|
|
209
|
+
display: flex;
|
|
210
|
+
flex-direction: column;
|
|
211
|
+
gap: 14px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
[data-myna="thread-header"] {
|
|
215
|
+
display: flex;
|
|
216
|
+
align-items: baseline;
|
|
217
|
+
gap: 10px;
|
|
218
|
+
flex-wrap: wrap;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
[data-myna="thread-title"] {
|
|
222
|
+
font-size: 18px;
|
|
223
|
+
font-weight: 600;
|
|
224
|
+
margin: 0;
|
|
225
|
+
flex: 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
[data-myna="thread-body"] {
|
|
229
|
+
margin: 0;
|
|
230
|
+
white-space: pre-wrap;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
[data-myna="timeline"] {
|
|
234
|
+
list-style: none;
|
|
235
|
+
margin: 0;
|
|
236
|
+
padding: 0 0 0 14px;
|
|
237
|
+
border-left: 1px solid var(--_line);
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
240
|
+
gap: 12px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
[data-myna="event-body"] {
|
|
244
|
+
margin: 0;
|
|
245
|
+
white-space: pre-wrap;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* A state change, not something anybody wrote — set back so the conversation
|
|
249
|
+
reads as the conversation. */
|
|
250
|
+
[data-myna="event-summary"] {
|
|
251
|
+
margin: 0;
|
|
252
|
+
color: var(--_muted);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* The reporter's own words, set apart from the team's, so a thread reads as a
|
|
256
|
+
conversation rather than a log. */
|
|
257
|
+
[data-myna="event"][data-myna-author="you"] [data-myna="event-body"] {
|
|
258
|
+
color: var(--_muted);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
[data-myna="event-time"] {
|
|
262
|
+
font-size: 12px;
|
|
263
|
+
color: var(--_muted);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
[data-myna="retest"],
|
|
267
|
+
[data-myna="reply"] {
|
|
268
|
+
display: flex;
|
|
269
|
+
gap: 8px;
|
|
270
|
+
align-items: flex-start;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
[data-myna="reply"] {
|
|
274
|
+
flex-direction: column;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
[data-myna="reply-input"] {
|
|
278
|
+
font: inherit;
|
|
279
|
+
color: inherit;
|
|
280
|
+
width: 100%;
|
|
281
|
+
box-sizing: border-box;
|
|
282
|
+
padding: 8px 10px;
|
|
283
|
+
border: 1px solid var(--_line);
|
|
284
|
+
border-radius: var(--_radius);
|
|
285
|
+
background: transparent;
|
|
286
|
+
}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myna-sh/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
".": {
|
|
13
13
|
"types": "./dist/index.d.ts",
|
|
14
14
|
"import": "./dist/index.js"
|
|
15
|
-
}
|
|
15
|
+
},
|
|
16
|
+
"./feedback": {
|
|
17
|
+
"types": "./dist/feedback/index.d.ts",
|
|
18
|
+
"import": "./dist/feedback/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./feedback.css": "./dist/feedback.css"
|
|
16
21
|
},
|
|
17
22
|
"main": "./dist/index.js",
|
|
18
23
|
"types": "./dist/index.d.ts",
|
|
@@ -22,9 +27,12 @@
|
|
|
22
27
|
"LICENSE"
|
|
23
28
|
],
|
|
24
29
|
"peerDependencies": {
|
|
25
|
-
"@myna-sh/sdk": "^0.
|
|
30
|
+
"@myna-sh/sdk": "^0.16.0",
|
|
26
31
|
"react": ">=18"
|
|
27
32
|
},
|
|
33
|
+
"sideEffects": [
|
|
34
|
+
"*.css"
|
|
35
|
+
],
|
|
28
36
|
"devDependencies": {
|
|
29
37
|
"@types/node": "24.13.3",
|
|
30
38
|
"@types/react": "19.2.17",
|
|
@@ -32,7 +40,7 @@
|
|
|
32
40
|
"react": "19.2.8",
|
|
33
41
|
"typescript": "5.9.3",
|
|
34
42
|
"@myna-sh/config": "0.0.0",
|
|
35
|
-
"@myna-sh/sdk": "0.
|
|
43
|
+
"@myna-sh/sdk": "0.16.0"
|
|
36
44
|
},
|
|
37
45
|
"scripts": {
|
|
38
46
|
"build": "tsup",
|