@dowel-ui/react 0.3.0 → 0.5.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.
Files changed (206) hide show
  1. package/README.md +13 -8
  2. package/dist/components/ai-disclosure/ai-disclosure.d.ts +92 -0
  3. package/dist/components/ai-disclosure/ai-disclosure.d.ts.map +1 -0
  4. package/dist/components/ai-disclosure/ai-disclosure.js +122 -0
  5. package/dist/components/ai-disclosure/ai-disclosure.js.map +1 -0
  6. package/dist/components/ai-disclosure/index.d.ts +2 -0
  7. package/dist/components/ai-disclosure/index.js +2 -0
  8. package/dist/components/ai-disclosure/meta.js +17 -0
  9. package/dist/components/ai-disclosure/meta.js.map +1 -0
  10. package/dist/components/ai-extraction-review/ai-extraction-review.d.ts +52 -0
  11. package/dist/components/ai-extraction-review/ai-extraction-review.d.ts.map +1 -0
  12. package/dist/components/ai-extraction-review/ai-extraction-review.js +249 -0
  13. package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -0
  14. package/dist/components/ai-extraction-review/extraction-model.d.ts +94 -0
  15. package/dist/components/ai-extraction-review/extraction-model.d.ts.map +1 -0
  16. package/dist/components/ai-extraction-review/extraction-model.js +91 -0
  17. package/dist/components/ai-extraction-review/extraction-model.js.map +1 -0
  18. package/dist/components/ai-extraction-review/index.d.ts +3 -0
  19. package/dist/components/ai-extraction-review/index.js +3 -0
  20. package/dist/components/ai-extraction-review/meta.js +17 -0
  21. package/dist/components/ai-extraction-review/meta.js.map +1 -0
  22. package/dist/components/ai-suggested-value/ai-suggested-value.d.ts +72 -0
  23. package/dist/components/ai-suggested-value/ai-suggested-value.d.ts.map +1 -0
  24. package/dist/components/ai-suggested-value/ai-suggested-value.js +126 -0
  25. package/dist/components/ai-suggested-value/ai-suggested-value.js.map +1 -0
  26. package/dist/components/ai-suggested-value/index.d.ts +2 -0
  27. package/dist/components/ai-suggested-value/index.js +2 -0
  28. package/dist/components/ai-suggested-value/meta.js +17 -0
  29. package/dist/components/ai-suggested-value/meta.js.map +1 -0
  30. package/dist/components/alert/alert.d.ts +1 -1
  31. package/dist/components/avatar/avatar.d.ts +1 -1
  32. package/dist/components/badge/badge.d.ts +1 -1
  33. package/dist/components/button/button.d.ts +1 -1
  34. package/dist/components/confirm-typed/confirm-typed.d.ts +47 -0
  35. package/dist/components/confirm-typed/confirm-typed.d.ts.map +1 -0
  36. package/dist/components/confirm-typed/confirm-typed.js +106 -0
  37. package/dist/components/confirm-typed/confirm-typed.js.map +1 -0
  38. package/dist/components/confirm-typed/index.d.ts +2 -0
  39. package/dist/components/confirm-typed/index.js +2 -0
  40. package/dist/components/confirm-typed/meta.js +17 -0
  41. package/dist/components/confirm-typed/meta.js.map +1 -0
  42. package/dist/components/cron-editor/cron-editor.d.ts +36 -0
  43. package/dist/components/cron-editor/cron-editor.d.ts.map +1 -0
  44. package/dist/components/cron-editor/cron-editor.js +493 -0
  45. package/dist/components/cron-editor/cron-editor.js.map +1 -0
  46. package/dist/components/cron-editor/cron-expression.d.ts +82 -0
  47. package/dist/components/cron-editor/cron-expression.d.ts.map +1 -0
  48. package/dist/components/cron-editor/cron-expression.js +352 -0
  49. package/dist/components/cron-editor/cron-expression.js.map +1 -0
  50. package/dist/components/cron-editor/index.d.ts +3 -0
  51. package/dist/components/cron-editor/index.js +3 -0
  52. package/dist/components/cron-editor/meta.js +17 -0
  53. package/dist/components/cron-editor/meta.js.map +1 -0
  54. package/dist/components/date-picker/date-picker.js +1 -1
  55. package/dist/components/diff-viewer/diff-model.d.ts +69 -0
  56. package/dist/components/diff-viewer/diff-model.d.ts.map +1 -0
  57. package/dist/components/diff-viewer/diff-model.js +180 -0
  58. package/dist/components/diff-viewer/diff-model.js.map +1 -0
  59. package/dist/components/diff-viewer/diff-viewer.d.ts +42 -0
  60. package/dist/components/diff-viewer/diff-viewer.d.ts.map +1 -0
  61. package/dist/components/diff-viewer/diff-viewer.js +212 -0
  62. package/dist/components/diff-viewer/diff-viewer.js.map +1 -0
  63. package/dist/components/diff-viewer/index.d.ts +3 -0
  64. package/dist/components/diff-viewer/index.js +3 -0
  65. package/dist/components/diff-viewer/meta.js +17 -0
  66. package/dist/components/diff-viewer/meta.js.map +1 -0
  67. package/dist/components/dns-record/dns-record.d.ts +61 -0
  68. package/dist/components/dns-record/dns-record.d.ts.map +1 -0
  69. package/dist/components/dns-record/dns-record.js +227 -0
  70. package/dist/components/dns-record/dns-record.js.map +1 -0
  71. package/dist/components/dns-record/index.d.ts +2 -0
  72. package/dist/components/dns-record/index.js +2 -0
  73. package/dist/components/dns-record/meta.js +17 -0
  74. package/dist/components/dns-record/meta.js.map +1 -0
  75. package/dist/components/log-viewer/index.d.ts +3 -0
  76. package/dist/components/log-viewer/index.js +3 -0
  77. package/dist/components/log-viewer/log-stream.d.ts +68 -0
  78. package/dist/components/log-viewer/log-stream.d.ts.map +1 -0
  79. package/dist/components/log-viewer/log-stream.js +120 -0
  80. package/dist/components/log-viewer/log-stream.js.map +1 -0
  81. package/dist/components/log-viewer/log-viewer.d.ts +51 -0
  82. package/dist/components/log-viewer/log-viewer.d.ts.map +1 -0
  83. package/dist/components/log-viewer/log-viewer.js +284 -0
  84. package/dist/components/log-viewer/log-viewer.js.map +1 -0
  85. package/dist/components/log-viewer/meta.js +17 -0
  86. package/dist/components/log-viewer/meta.js.map +1 -0
  87. package/dist/components/permission-matrix/index.d.ts +3 -0
  88. package/dist/components/permission-matrix/index.js +3 -0
  89. package/dist/components/permission-matrix/meta.js +17 -0
  90. package/dist/components/permission-matrix/meta.js.map +1 -0
  91. package/dist/components/permission-matrix/permission-matrix.d.ts +46 -0
  92. package/dist/components/permission-matrix/permission-matrix.d.ts.map +1 -0
  93. package/dist/components/permission-matrix/permission-matrix.js +199 -0
  94. package/dist/components/permission-matrix/permission-matrix.js.map +1 -0
  95. package/dist/components/permission-matrix/permission-model.d.ts +53 -0
  96. package/dist/components/permission-matrix/permission-model.d.ts.map +1 -0
  97. package/dist/components/permission-matrix/permission-model.js +64 -0
  98. package/dist/components/permission-matrix/permission-model.js.map +1 -0
  99. package/dist/components/progress/progress.d.ts +1 -1
  100. package/dist/components/secret-field/index.d.ts +2 -0
  101. package/dist/components/secret-field/index.js +2 -0
  102. package/dist/components/secret-field/meta.js +17 -0
  103. package/dist/components/secret-field/meta.js.map +1 -0
  104. package/dist/components/secret-field/secret-field.d.ts +58 -0
  105. package/dist/components/secret-field/secret-field.d.ts.map +1 -0
  106. package/dist/components/secret-field/secret-field.js +168 -0
  107. package/dist/components/secret-field/secret-field.js.map +1 -0
  108. package/dist/components/session-expiry/index.d.ts +2 -0
  109. package/dist/components/session-expiry/index.js +2 -0
  110. package/dist/components/session-expiry/meta.js +17 -0
  111. package/dist/components/session-expiry/meta.js.map +1 -0
  112. package/dist/components/session-expiry/session-expiry.d.ts +64 -0
  113. package/dist/components/session-expiry/session-expiry.d.ts.map +1 -0
  114. package/dist/components/session-expiry/session-expiry.js +185 -0
  115. package/dist/components/session-expiry/session-expiry.js.map +1 -0
  116. package/dist/components/shortcut-recorder/index.d.ts +3 -0
  117. package/dist/components/shortcut-recorder/index.js +3 -0
  118. package/dist/components/shortcut-recorder/meta.js +17 -0
  119. package/dist/components/shortcut-recorder/meta.js.map +1 -0
  120. package/dist/components/shortcut-recorder/shortcut-model.d.ts +67 -0
  121. package/dist/components/shortcut-recorder/shortcut-model.d.ts.map +1 -0
  122. package/dist/components/shortcut-recorder/shortcut-model.js +168 -0
  123. package/dist/components/shortcut-recorder/shortcut-model.js.map +1 -0
  124. package/dist/components/shortcut-recorder/shortcut-recorder.d.ts +47 -0
  125. package/dist/components/shortcut-recorder/shortcut-recorder.d.ts.map +1 -0
  126. package/dist/components/shortcut-recorder/shortcut-recorder.js +212 -0
  127. package/dist/components/shortcut-recorder/shortcut-recorder.js.map +1 -0
  128. package/dist/components/spinner/spinner.d.ts +1 -1
  129. package/dist/components/sync-status/index.d.ts +2 -0
  130. package/dist/components/sync-status/index.js +2 -0
  131. package/dist/components/sync-status/meta.js +17 -0
  132. package/dist/components/sync-status/meta.js.map +1 -0
  133. package/dist/components/sync-status/sync-status.d.ts +65 -0
  134. package/dist/components/sync-status/sync-status.d.ts.map +1 -0
  135. package/dist/components/sync-status/sync-status.js +103 -0
  136. package/dist/components/sync-status/sync-status.js.map +1 -0
  137. package/dist/components/time-range-picker/index.d.ts +3 -0
  138. package/dist/components/time-range-picker/index.js +3 -0
  139. package/dist/components/time-range-picker/meta.js +22 -0
  140. package/dist/components/time-range-picker/meta.js.map +1 -0
  141. package/dist/components/time-range-picker/time-expression.d.ts +88 -0
  142. package/dist/components/time-range-picker/time-expression.d.ts.map +1 -0
  143. package/dist/components/time-range-picker/time-expression.js +335 -0
  144. package/dist/components/time-range-picker/time-expression.js.map +1 -0
  145. package/dist/components/time-range-picker/time-range-picker.d.ts +65 -0
  146. package/dist/components/time-range-picker/time-range-picker.d.ts.map +1 -0
  147. package/dist/components/time-range-picker/time-range-picker.js +377 -0
  148. package/dist/components/time-range-picker/time-range-picker.js.map +1 -0
  149. package/dist/components/toast/toast.d.ts +1 -1
  150. package/dist/index.d.ts +36 -1
  151. package/dist/index.js +24 -3
  152. package/dist/registry/components.d.ts.map +1 -1
  153. package/dist/registry/components.js +78 -50
  154. package/dist/registry/components.js.map +1 -1
  155. package/package.json +6 -4
  156. package/src/components/ai-disclosure/ai-disclosure.tsx +244 -0
  157. package/src/components/ai-disclosure/index.ts +9 -0
  158. package/src/components/ai-disclosure/meta.ts +23 -0
  159. package/src/components/ai-extraction-review/ai-extraction-review.tsx +492 -0
  160. package/src/components/ai-extraction-review/extraction-model.ts +159 -0
  161. package/src/components/ai-extraction-review/index.ts +13 -0
  162. package/src/components/ai-extraction-review/meta.ts +24 -0
  163. package/src/components/ai-suggested-value/ai-suggested-value.tsx +295 -0
  164. package/src/components/ai-suggested-value/index.ts +6 -0
  165. package/src/components/ai-suggested-value/meta.ts +24 -0
  166. package/src/components/confirm-typed/confirm-typed.tsx +191 -0
  167. package/src/components/confirm-typed/index.ts +1 -0
  168. package/src/components/confirm-typed/meta.ts +22 -0
  169. package/src/components/cron-editor/cron-editor.tsx +676 -0
  170. package/src/components/cron-editor/cron-expression.ts +552 -0
  171. package/src/components/cron-editor/index.ts +23 -0
  172. package/src/components/cron-editor/meta.ts +23 -0
  173. package/src/components/diff-viewer/diff-model.ts +256 -0
  174. package/src/components/diff-viewer/diff-viewer.tsx +339 -0
  175. package/src/components/diff-viewer/index.ts +17 -0
  176. package/src/components/diff-viewer/meta.ts +20 -0
  177. package/src/components/dns-record/dns-record.tsx +315 -0
  178. package/src/components/dns-record/index.ts +7 -0
  179. package/src/components/dns-record/meta.ts +21 -0
  180. package/src/components/log-viewer/index.ts +21 -0
  181. package/src/components/log-viewer/log-stream.ts +176 -0
  182. package/src/components/log-viewer/log-viewer.tsx +424 -0
  183. package/src/components/log-viewer/meta.ts +21 -0
  184. package/src/components/permission-matrix/index.ts +14 -0
  185. package/src/components/permission-matrix/meta.ts +23 -0
  186. package/src/components/permission-matrix/permission-matrix.tsx +368 -0
  187. package/src/components/permission-matrix/permission-model.ts +120 -0
  188. package/src/components/secret-field/index.ts +6 -0
  189. package/src/components/secret-field/meta.ts +21 -0
  190. package/src/components/secret-field/secret-field.tsx +284 -0
  191. package/src/components/session-expiry/index.ts +9 -0
  192. package/src/components/session-expiry/meta.ts +23 -0
  193. package/src/components/session-expiry/session-expiry.tsx +321 -0
  194. package/src/components/shortcut-recorder/index.ts +19 -0
  195. package/src/components/shortcut-recorder/meta.ts +22 -0
  196. package/src/components/shortcut-recorder/shortcut-model.ts +204 -0
  197. package/src/components/shortcut-recorder/shortcut-recorder.tsx +336 -0
  198. package/src/components/sync-status/index.ts +9 -0
  199. package/src/components/sync-status/meta.ts +21 -0
  200. package/src/components/sync-status/sync-status.tsx +217 -0
  201. package/src/components/time-range-picker/index.ts +25 -0
  202. package/src/components/time-range-picker/meta.ts +22 -0
  203. package/src/components/time-range-picker/time-expression.ts +491 -0
  204. package/src/components/time-range-picker/time-range-picker.tsx +522 -0
  205. package/src/index.ts +14 -0
  206. package/src/registry/components.ts +28 -0
@@ -0,0 +1,227 @@
1
+ "use client";
2
+ import { focusRing } from "../../lib/styles.js";
3
+ import { cn } from "../../lib/utils.js";
4
+ import { Spinner } from "../spinner/spinner.js";
5
+ import { Button } from "../button/button.js";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import { useEffect, useId, useRef, useState } from "react";
8
+ //#region src/components/dns-record/dns-record.tsx
9
+ /**
10
+ * The host both ways a provider might want it. Accepts either form and a
11
+ * trailing dot, so the record can be declared however the backend has it.
12
+ */
13
+ function dnsHostForms(name, zone) {
14
+ const cleanZone = zone.replace(/\.$/, "");
15
+ const cleanName = name.replace(/\.$/, "");
16
+ if (cleanName === "@" || cleanName === "" || cleanName === cleanZone) return {
17
+ relative: "@",
18
+ full: cleanZone
19
+ };
20
+ if (cleanName.endsWith(`.${cleanZone}`)) return {
21
+ relative: cleanName.slice(0, -cleanZone.length - 1),
22
+ full: cleanName
23
+ };
24
+ return {
25
+ relative: cleanName,
26
+ full: `${cleanName}.${cleanZone}`
27
+ };
28
+ }
29
+ function DnsRecord({ className, type, name, value, zone, ttl, priority, purpose, status = "pending", found, checkedAt, locale, onCheck, children, ...props }) {
30
+ const id = useId();
31
+ const forms = zone ? dnsHostForms(name, zone) : null;
32
+ const host = forms ? forms.relative : name;
33
+ const [copy, setCopy] = useState(null);
34
+ const timer = useRef(null);
35
+ useEffect(() => () => {
36
+ if (timer.current) clearTimeout(timer.current);
37
+ }, []);
38
+ const copyPart = (part, text) => {
39
+ const settle = (state) => {
40
+ setCopy({
41
+ part,
42
+ state
43
+ });
44
+ if (timer.current) clearTimeout(timer.current);
45
+ timer.current = setTimeout(() => {
46
+ setCopy(null);
47
+ }, 2e3);
48
+ };
49
+ Promise.resolve().then(() => navigator.clipboard.writeText(text)).then(() => {
50
+ settle("copied");
51
+ }, () => {
52
+ settle("error");
53
+ });
54
+ };
55
+ const statusText = status === "verified" ? "Verified" : status === "checking" ? "Checking…" : status === "failed" ? found && found.length > 0 ? "Not verified. Something else was found." : "Not verified. Nothing found yet — DNS changes can take up to 48 hours to appear." : "Not checked yet";
56
+ const checked = checkedAt !== void 0 ? new Intl.DateTimeFormat(locale, {
57
+ dateStyle: "medium",
58
+ timeStyle: "short"
59
+ }).format(checkedAt) : null;
60
+ const parts = [
61
+ {
62
+ key: "type",
63
+ label: "Type",
64
+ text: type
65
+ },
66
+ {
67
+ key: "name",
68
+ label: "Name",
69
+ text: host,
70
+ note: forms ? forms.relative === "@" ? /* @__PURE__ */ jsxs(Fragment, { children: [
71
+ "Some providers want ",
72
+ /* @__PURE__ */ jsx("code", {
73
+ className: "font-mono",
74
+ children: forms.full
75
+ }),
76
+ " or leave it blank instead of ",
77
+ /* @__PURE__ */ jsx("code", {
78
+ className: "font-mono",
79
+ children: "@"
80
+ }),
81
+ "."
82
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
83
+ "Some providers want the full name, ",
84
+ /* @__PURE__ */ jsx("code", {
85
+ className: "font-mono",
86
+ children: forms.full
87
+ }),
88
+ ". If the field already ends in ",
89
+ /* @__PURE__ */ jsx("code", {
90
+ className: "font-mono",
91
+ children: zone
92
+ }),
93
+ ", enter just the part shown."
94
+ ] }) : void 0
95
+ },
96
+ {
97
+ key: "value",
98
+ label: "Value",
99
+ text: value,
100
+ note: type.toUpperCase() === "TXT" ? "Enter without quotes; your provider adds them if it needs to." : void 0
101
+ },
102
+ ...priority !== void 0 ? [{
103
+ key: "priority",
104
+ label: "Priority",
105
+ text: String(priority)
106
+ }] : [],
107
+ ...ttl !== void 0 ? [{
108
+ key: "ttl",
109
+ label: "TTL",
110
+ text: String(ttl)
111
+ }] : []
112
+ ];
113
+ return /* @__PURE__ */ jsxs("section", {
114
+ "data-slot": "dns-record",
115
+ "data-status": status,
116
+ "aria-labelledby": `${id}-heading`,
117
+ "aria-busy": status === "checking" || void 0,
118
+ className: cn("flex flex-col gap-3 rounded-lg border border-border bg-card p-4", className),
119
+ ...props,
120
+ children: [
121
+ /* @__PURE__ */ jsxs("div", {
122
+ className: "flex flex-wrap items-baseline justify-between gap-2",
123
+ children: [/* @__PURE__ */ jsxs("h3", {
124
+ id: `${id}-heading`,
125
+ className: "text-sm font-medium",
126
+ children: [
127
+ type,
128
+ " record",
129
+ purpose ? /* @__PURE__ */ jsxs("span", {
130
+ className: "font-normal text-muted-foreground",
131
+ children: [" · ", purpose]
132
+ }) : null
133
+ ]
134
+ }), /* @__PURE__ */ jsxs("p", {
135
+ "data-slot": "dns-record-status",
136
+ role: "status",
137
+ "aria-live": "polite",
138
+ className: cn("flex items-center gap-1.5 text-xs", status === "verified" && "text-success", status === "failed" && "text-destructive", (status === "pending" || status === "checking") && "text-muted-foreground"),
139
+ children: [
140
+ status === "checking" ? /* @__PURE__ */ jsx(Spinner, { size: "sm" }) : null,
141
+ statusText,
142
+ checked && status !== "checking" ? /* @__PURE__ */ jsxs("span", {
143
+ className: "text-muted-foreground",
144
+ children: [" · checked ", checked]
145
+ }) : null
146
+ ]
147
+ })]
148
+ }),
149
+ children,
150
+ /* @__PURE__ */ jsx("dl", {
151
+ className: "m-0 grid gap-x-4 gap-y-2 sm:grid-cols-[auto_1fr]",
152
+ children: parts.map((part) => /* @__PURE__ */ jsxs("div", {
153
+ className: "contents",
154
+ children: [/* @__PURE__ */ jsx("dt", {
155
+ className: "text-xs font-medium text-muted-foreground sm:pt-1.5",
156
+ children: part.label
157
+ }), /* @__PURE__ */ jsxs("dd", {
158
+ className: "m-0 flex flex-col gap-1",
159
+ children: [
160
+ /* @__PURE__ */ jsxs("div", {
161
+ className: "flex items-start gap-2",
162
+ children: [/* @__PURE__ */ jsx("code", {
163
+ "data-slot": "dns-record-part",
164
+ "data-part": part.key,
165
+ className: "min-w-0 flex-1 rounded-md bg-muted px-2 py-1 font-mono text-xs break-all",
166
+ children: part.text
167
+ }), /* @__PURE__ */ jsx("button", {
168
+ type: "button",
169
+ "aria-label": `Copy ${part.label.toLowerCase()}`,
170
+ "data-state": copy?.part === part.key ? copy.state : "idle",
171
+ className: cn("shrink-0 rounded-md border border-input bg-background px-2 py-1 text-xs font-medium", "hover:bg-accent hover:text-accent-foreground", focusRing),
172
+ onClick: () => {
173
+ copyPart(part.key, part.text);
174
+ },
175
+ children: copy?.part === part.key && copy.state === "copied" ? "Copied" : "Copy"
176
+ })]
177
+ }),
178
+ copy?.part === part.key && copy.state === "error" ? /* @__PURE__ */ jsx("p", {
179
+ "data-slot": "dns-record-copy-error",
180
+ className: "text-xs text-destructive",
181
+ children: "Could not copy. Select it and copy by hand."
182
+ }) : null,
183
+ part.note ? /* @__PURE__ */ jsx("p", {
184
+ className: "text-xs text-muted-foreground",
185
+ children: part.note
186
+ }) : null
187
+ ]
188
+ })]
189
+ }, part.key))
190
+ }),
191
+ status === "failed" && found && found.length > 0 ? /* @__PURE__ */ jsxs("div", {
192
+ "data-slot": "dns-record-found",
193
+ className: "rounded-md border border-destructive/40 bg-destructive/5 p-3 text-xs",
194
+ children: [/* @__PURE__ */ jsx("p", {
195
+ className: "font-medium text-destructive",
196
+ children: "Found instead:"
197
+ }), /* @__PURE__ */ jsx("ul", {
198
+ className: "m-0 mt-1 list-none p-0",
199
+ children: found.map((entry) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", {
200
+ className: "font-mono break-all",
201
+ children: entry
202
+ }) }, entry))
203
+ })]
204
+ }) : null,
205
+ /* @__PURE__ */ jsx("span", {
206
+ role: "status",
207
+ "aria-live": "polite",
208
+ className: "sr-only",
209
+ children: copy?.state === "copied" ? `Copied ${copy.part}` : copy?.state === "error" ? `Could not copy ${copy.part}. Select it and copy by hand.` : ""
210
+ }),
211
+ onCheck ? /* @__PURE__ */ jsx("div", {
212
+ className: "flex items-center gap-2",
213
+ children: /* @__PURE__ */ jsx(Button, {
214
+ variant: "outline",
215
+ size: "sm",
216
+ loading: status === "checking",
217
+ onClick: onCheck,
218
+ children: status === "verified" ? "Check again" : "Check now"
219
+ })
220
+ }) : null
221
+ ]
222
+ });
223
+ }
224
+ //#endregion
225
+ export { DnsRecord, dnsHostForms };
226
+
227
+ //# sourceMappingURL=dns-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dns-record.js","names":[],"sources":["../../../src/components/dns-record/dns-record.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useEffect,\n useId,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type ReactNode,\n} from \"react\";\n\nimport { Button } from \"@/components/button\";\nimport { Spinner } from \"@/components/spinner\";\nimport { focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * \"Add this record at your DNS provider.\"\n *\n * Vercel, Resend, Cloudflare, Postmark, Google Workspace and every product\n * that verifies a domain or routes its mail draws this card by hand, and\n * they all learn the same three things the hard way.\n *\n * The parts are copied separately. A provider's form has a Name field, a\n * Type field and a Value field, so one button that copies the whole line\n * copies something nobody can paste anywhere.\n *\n * The Name field is a trap. Some providers want the host relative to the\n * zone, `_dmarc`; some want the full name, `_dmarc.acme.com`; and some take\n * the relative form and append the zone themselves, so a full name pasted\n * in becomes `_dmarc.acme.com.acme.com` and the check fails for a reason\n * nobody can see. So the host is shown both ways, with the sentence that\n * explains which to use.\n *\n * A failed check says what was found. \"Not verified\" sends people back to\n * their provider to stare at a record that is correct and has not\n * propagated yet; \"found v=spf1 -all, expected v=spf1 include:…\" sends them\n * to the typo. And nothing found at all is said as nothing found, with how\n * long that can take, because it is not the same as wrong.\n */\n\nexport type DnsRecordStatus = \"pending\" | \"checking\" | \"verified\" | \"failed\";\n\nexport interface DnsHostForms {\n /** Relative to the zone: `_dmarc`, or `@` for the zone itself. */\n relative: string;\n /** Fully qualified: `_dmarc.acme.com`. */\n full: string;\n}\n\n/**\n * The host both ways a provider might want it. Accepts either form and a\n * trailing dot, so the record can be declared however the backend has it.\n */\nexport function dnsHostForms(name: string, zone: string): DnsHostForms {\n const cleanZone = zone.replace(/\\.$/, \"\");\n const cleanName = name.replace(/\\.$/, \"\");\n if (cleanName === \"@\" || cleanName === \"\" || cleanName === cleanZone) {\n return { relative: \"@\", full: cleanZone };\n }\n if (cleanName.endsWith(`.${cleanZone}`)) {\n return { relative: cleanName.slice(0, -cleanZone.length - 1), full: cleanName };\n }\n return { relative: cleanName, full: `${cleanName}.${cleanZone}` };\n}\n\nexport interface DnsRecordProps extends Omit<ComponentPropsWithRef<\"section\">, \"children\"> {\n type: string;\n /** The host, relative or fully qualified. */\n name: string;\n value: string;\n /** The zone the record belongs to. Enables the host to be shown both ways. */\n zone?: string;\n ttl?: number | string;\n priority?: number;\n /** What the record is for, in the reader's language: \"Proves you own acme.com\". */\n purpose?: string;\n status?: DnsRecordStatus;\n /** What the check found instead, when it failed. The useful part. */\n found?: string[];\n checkedAt?: Date;\n locale?: string;\n onCheck?: () => void;\n children?: ReactNode;\n}\n\ntype CopyState = \"idle\" | \"copied\" | \"error\";\n\nexport function DnsRecord({\n className,\n type,\n name,\n value,\n zone,\n ttl,\n priority,\n purpose,\n status = \"pending\",\n found,\n checkedAt,\n locale,\n onCheck,\n children,\n ...props\n}: DnsRecordProps) {\n const id = useId();\n const forms = zone ? dnsHostForms(name, zone) : null;\n const host = forms ? forms.relative : name;\n\n // Which part was copied, and whether it worked, so the button that was\n // pressed is the one that says so.\n const [copy, setCopy] = useState<{ part: string; state: CopyState } | null>(null);\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(\n () => () => {\n if (timer.current) clearTimeout(timer.current);\n },\n [],\n );\n\n const copyPart = (part: string, text: string) => {\n const settle = (state: CopyState) => {\n setCopy({ part, state });\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(() => {\n setCopy(null);\n }, 2000);\n };\n Promise.resolve()\n .then(() => navigator.clipboard.writeText(text))\n .then(\n () => {\n settle(\"copied\");\n },\n () => {\n settle(\"error\");\n },\n );\n };\n\n const statusText =\n status === \"verified\"\n ? \"Verified\"\n : status === \"checking\"\n ? \"Checking…\"\n : status === \"failed\"\n ? found && found.length > 0\n ? \"Not verified. Something else was found.\"\n : \"Not verified. Nothing found yet — DNS changes can take up to 48 hours to appear.\"\n : \"Not checked yet\";\n\n const checked =\n checkedAt !== undefined\n ? new Intl.DateTimeFormat(locale, { dateStyle: \"medium\", timeStyle: \"short\" }).format(\n checkedAt,\n )\n : null;\n\n const parts: { key: string; label: string; text: string; note?: ReactNode }[] = [\n { key: \"type\", label: \"Type\", text: type },\n {\n key: \"name\",\n label: \"Name\",\n text: host,\n note: forms ? (\n forms.relative === \"@\" ? (\n <>\n Some providers want <code className=\"font-mono\">{forms.full}</code> or leave it\n blank instead of <code className=\"font-mono\">@</code>.\n </>\n ) : (\n <>\n Some providers want the full name, <code className=\"font-mono\">{forms.full}</code>.\n If the field already ends in <code className=\"font-mono\">{zone}</code>, enter just\n the part shown.\n </>\n )\n ) : undefined,\n },\n {\n key: \"value\",\n label: \"Value\",\n text: value,\n note:\n type.toUpperCase() === \"TXT\"\n ? \"Enter without quotes; your provider adds them if it needs to.\"\n : undefined,\n },\n ...(priority !== undefined\n ? [{ key: \"priority\", label: \"Priority\", text: String(priority) }]\n : []),\n ...(ttl !== undefined ? [{ key: \"ttl\", label: \"TTL\", text: String(ttl) }] : []),\n ];\n\n return (\n <section\n data-slot=\"dns-record\"\n data-status={status}\n aria-labelledby={`${id}-heading`}\n aria-busy={status === \"checking\" || undefined}\n className={cn(\n \"flex flex-col gap-3 rounded-lg border border-border bg-card p-4\",\n className,\n )}\n {...props}\n >\n <div className=\"flex flex-wrap items-baseline justify-between gap-2\">\n <h3 id={`${id}-heading`} className=\"text-sm font-medium\">\n {type} record\n {purpose ? (\n <span className=\"font-normal text-muted-foreground\"> · {purpose}</span>\n ) : null}\n </h3>\n {/* Status as a word, and announced when it changes: a check was asked\n for, and the answer should arrive where the asker is. */}\n <p\n data-slot=\"dns-record-status\"\n role=\"status\"\n aria-live=\"polite\"\n className={cn(\n \"flex items-center gap-1.5 text-xs\",\n status === \"verified\" && \"text-success\",\n status === \"failed\" && \"text-destructive\",\n (status === \"pending\" || status === \"checking\") && \"text-muted-foreground\",\n )}\n >\n {status === \"checking\" ? <Spinner size=\"sm\" /> : null}\n {statusText}\n {checked && status !== \"checking\" ? (\n <span className=\"text-muted-foreground\"> · checked {checked}</span>\n ) : null}\n </p>\n </div>\n\n {children}\n\n <dl className=\"m-0 grid gap-x-4 gap-y-2 sm:grid-cols-[auto_1fr]\">\n {parts.map((part) => (\n <div key={part.key} className=\"contents\">\n <dt className=\"text-xs font-medium text-muted-foreground sm:pt-1.5\">\n {part.label}\n </dt>\n <dd className=\"m-0 flex flex-col gap-1\">\n <div className=\"flex items-start gap-2\">\n <code\n data-slot=\"dns-record-part\"\n data-part={part.key}\n className=\"min-w-0 flex-1 rounded-md bg-muted px-2 py-1 font-mono text-xs break-all\"\n >\n {part.text}\n </code>\n <button\n type=\"button\"\n aria-label={`Copy ${part.label.toLowerCase()}`}\n data-state={copy?.part === part.key ? copy.state : \"idle\"}\n className={cn(\n \"shrink-0 rounded-md border border-input bg-background px-2 py-1 text-xs font-medium\",\n \"hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n onClick={() => {\n copyPart(part.key, part.text);\n }}\n >\n {copy?.part === part.key && copy.state === \"copied\" ? \"Copied\" : \"Copy\"}\n </button>\n </div>\n {copy?.part === part.key && copy.state === \"error\" ? (\n <p data-slot=\"dns-record-copy-error\" className=\"text-xs text-destructive\">\n Could not copy. Select it and copy by hand.\n </p>\n ) : null}\n {part.note ? <p className=\"text-xs text-muted-foreground\">{part.note}</p> : null}\n </dd>\n </div>\n ))}\n </dl>\n\n {status === \"failed\" && found && found.length > 0 ? (\n <div\n data-slot=\"dns-record-found\"\n className=\"rounded-md border border-destructive/40 bg-destructive/5 p-3 text-xs\"\n >\n <p className=\"font-medium text-destructive\">Found instead:</p>\n <ul className=\"m-0 mt-1 list-none p-0\">\n {found.map((entry) => (\n <li key={entry}>\n <code className=\"font-mono break-all\">{entry}</code>\n </li>\n ))}\n </ul>\n </div>\n ) : null}\n\n {/* A separate region from the verification status, so \"Copied name\"\n never replaces or interrupts a check result that is being read.\n Present from the start, so the first copy is heard. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {copy?.state === \"copied\"\n ? `Copied ${copy.part}`\n : copy?.state === \"error\"\n ? `Could not copy ${copy.part}. Select it and copy by hand.`\n : \"\"}\n </span>\n\n {onCheck ? (\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" size=\"sm\" loading={status === \"checking\"} onClick={onCheck}>\n {status === \"verified\" ? \"Check again\" : \"Check now\"}\n </Button>\n </div>\n ) : null}\n </section>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { DnsHostForms, DnsRecord, DnsRecordProps, DnsRecordStatus, dnsHostForms } from "./dns-record.js";
2
+ export { type DnsHostForms, DnsRecord, type DnsRecordProps, type DnsRecordStatus, dnsHostForms };
@@ -0,0 +1,2 @@
1
+ import { DnsRecord, dnsHostForms } from "./dns-record.js";
2
+ export { DnsRecord, dnsHostForms };
@@ -0,0 +1,17 @@
1
+ import { defineMeta } from "../../registry/schema.js";
2
+ //#region src/components/dns-record/meta.ts
3
+ const meta = defineMeta({
4
+ name: "dns-record",
5
+ title: "DNS Record",
6
+ description: "Add this record at your provider: parts copied separately, checked with what was found.",
7
+ category: "data",
8
+ status: "stable",
9
+ dependencies: [],
10
+ registryDependencies: ["button", "spinner"],
11
+ files: ["dns-record.tsx"],
12
+ a11y: "The card is a region named by the record's type and purpose. Each part is a definition with its own copy button, named by the part it copies, so five Copy buttons are five different buttons. Verification status is a word in a polite live region, so the answer to a check arrives where it was asked for, and the card is aria-busy while checking. Copy results are announced in a separate region, present from the start, so a copy never replaces a check result mid-sentence; failure is announced with what to do instead. What the check found is a list, not a colour."
13
+ });
14
+ //#endregion
15
+ export { meta };
16
+
17
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/dns-record/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"dns-record\",\n title: \"DNS Record\",\n description:\n \"Add this record at your provider: parts copied separately, checked with what was found.\",\n category: \"data\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [\"button\", \"spinner\"],\n files: [\"dns-record.tsx\"],\n a11y:\n \"The card is a region named by the record's type and purpose. Each part is a definition \" +\n \"with its own copy button, named by the part it copies, so five Copy buttons are five \" +\n \"different buttons. Verification status is a word in a polite live region, so the answer to \" +\n \"a check arrives where it was asked for, and the card is aria-busy while checking. Copy \" +\n \"results are announced in a separate region, present from the start, so a copy never \" +\n \"replaces a check result mid-sentence; failure is announced with what to do instead. What \" +\n \"the check found is a list, not a colour.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aACE;CACF,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC,UAAU,SAAS;CAC1C,OAAO,CAAC,gBAAgB;CACxB,MACE;AAOJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { FilterState, LOG_LEVELS, LogLevel, LogLine, MatchRange, UseLogStreamOptions, VisibleLine, compileQuery, findMatches, segment, useLogStream } from "./log-stream.js";
2
+ import { LogViewer, LogViewerProps, LogViewerRow, LogViewerRowProps, LogViewerToolbar, LogViewerToolbarProps } from "./log-viewer.js";
3
+ export { type FilterState, LOG_LEVELS, type LogLevel, type LogLine, LogViewer, type LogViewerProps, LogViewerRow, type LogViewerRowProps, LogViewerToolbar, type LogViewerToolbarProps, type MatchRange, type UseLogStreamOptions, type VisibleLine, compileQuery, findMatches, segment, useLogStream };
@@ -0,0 +1,3 @@
1
+ import { LOG_LEVELS, compileQuery, findMatches, segment, useLogStream } from "./log-stream.js";
2
+ import { LogViewer, LogViewerRow, LogViewerToolbar } from "./log-viewer.js";
3
+ export { LOG_LEVELS, LogViewer, LogViewerRow, LogViewerToolbar, compileQuery, findMatches, segment, useLogStream };
@@ -0,0 +1,68 @@
1
+ //#region src/components/log-viewer/log-stream.d.ts
2
+ /**
3
+ * Filtering and match-finding for a log stream.
4
+ *
5
+ * Separate from the view because it is pure and worth testing without a DOM,
6
+ * and because the awkward parts are here rather than in the rendering: a bad
7
+ * regex must not throw while someone is halfway through typing it, and a match
8
+ * has to be located precisely enough to highlight rather than merely detected.
9
+ */
10
+ type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
11
+ declare const LOG_LEVELS: readonly LogLevel[];
12
+ interface LogLine {
13
+ id: string;
14
+ message: string;
15
+ level?: LogLevel;
16
+ /** ISO timestamp, or anything the consumer wants shown in the gutter. */
17
+ timestamp?: string;
18
+ /** Structured fields, revealed when the row is expanded. */
19
+ fields?: Record<string, unknown>;
20
+ }
21
+ /** A [start, end) slice of a message that matched the filter. */
22
+ type MatchRange = readonly [number, number];
23
+ interface FilterState {
24
+ /** Substring, or a pattern when `regex` is on. */
25
+ query: string;
26
+ regex: boolean;
27
+ /** Levels to show. Empty means all of them. */
28
+ levels: Set<LogLevel>;
29
+ }
30
+ /**
31
+ * Compiles the query.
32
+ *
33
+ * Returns null for an invalid pattern rather than throwing: the query is being
34
+ * typed, so it spends most of its life syntactically incomplete, and a viewer
35
+ * that crashes on "(" is unusable.
36
+ */
37
+ declare function compileQuery(query: string, regex: boolean): RegExp | null;
38
+ /** Every match in a line, so each can be highlighted rather than just the first. */
39
+ declare function findMatches(message: string, pattern: RegExp | null): MatchRange[];
40
+ /** Splits a message into alternating plain and matched segments. */
41
+ declare function segment(message: string, ranges: MatchRange[]): {
42
+ text: string;
43
+ match: boolean;
44
+ }[];
45
+ interface VisibleLine extends LogLine {
46
+ matches: MatchRange[];
47
+ }
48
+ interface UseLogStreamOptions {
49
+ lines: LogLine[];
50
+ /** Levels present but unchecked are hidden. Empty shows everything. */
51
+ initialLevels?: Set<LogLevel>;
52
+ }
53
+ declare function useLogStream({ lines, initialLevels }: UseLogStreamOptions): {
54
+ query: string;
55
+ setQuery: import("react").Dispatch<import("react").SetStateAction<string>>;
56
+ regex: boolean;
57
+ setRegex: import("react").Dispatch<import("react").SetStateAction<boolean>>;
58
+ levels: Set<LogLevel>;
59
+ toggleLevel: (level: LogLevel) => void;
60
+ setLevels: import("react").Dispatch<import("react").SetStateAction<Set<LogLevel>>>;
61
+ visible: VisibleLine[];
62
+ counts: Map<LogLevel, number>;
63
+ invalidPattern: boolean;
64
+ total: number;
65
+ };
66
+ //#endregion
67
+ export { FilterState, LOG_LEVELS, LogLevel, LogLine, MatchRange, UseLogStreamOptions, VisibleLine, compileQuery, findMatches, segment, useLogStream };
68
+ //# sourceMappingURL=log-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-stream.d.ts","names":[],"sources":["../../../src/components/log-viewer/log-stream.ts"],"mappings":";;;;;;;;;KAaY;cAEC,qBAAqB;UASjB;EACf;EACA;EACA,QAAQ;;EAER;;EAEA,SAAS;;;KAIC;UAEK;;EAEf;EACA;;EAEA,QAAQ,IAAI;;;;;;;;;iBAUE,aAAa,eAAe,iBAAiB;;iBAY7C,YAAY,iBAAiB,SAAS,gBAAgB;;iBAuBtD,QACd,iBACA,QAAQ;EACL;EAAc;;UAgBF,oBAAoB;EACnC,SAAS;;UAGM;EACf,OAAO;;EAEP,gBAAgB,IAAI;;iBAGN,eAAe,OAAO,iBAAiB;;;;;;EAqCb,cAAA,OAAA"}
@@ -0,0 +1,120 @@
1
+ "use client";
2
+ import { useCallback, useMemo, useState } from "react";
3
+ //#region src/components/log-viewer/log-stream.ts
4
+ const LOG_LEVELS = [
5
+ "trace",
6
+ "debug",
7
+ "info",
8
+ "warn",
9
+ "error",
10
+ "fatal"
11
+ ];
12
+ /**
13
+ * Compiles the query.
14
+ *
15
+ * Returns null for an invalid pattern rather than throwing: the query is being
16
+ * typed, so it spends most of its life syntactically incomplete, and a viewer
17
+ * that crashes on "(" is unusable.
18
+ */
19
+ function compileQuery(query, regex) {
20
+ if (query.length === 0) return null;
21
+ try {
22
+ return regex ? new RegExp(query, "gi") : new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ /** Every match in a line, so each can be highlighted rather than just the first. */
28
+ function findMatches(message, pattern) {
29
+ if (!pattern) return [];
30
+ const ranges = [];
31
+ pattern.lastIndex = 0;
32
+ let match = pattern.exec(message);
33
+ while (match !== null) {
34
+ if (match[0].length === 0) pattern.lastIndex += 1;
35
+ else ranges.push([match.index, match.index + match[0].length]);
36
+ match = pattern.exec(message);
37
+ }
38
+ return ranges;
39
+ }
40
+ /** Splits a message into alternating plain and matched segments. */
41
+ function segment(message, ranges) {
42
+ if (ranges.length === 0) return [{
43
+ text: message,
44
+ match: false
45
+ }];
46
+ const parts = [];
47
+ let cursor = 0;
48
+ for (const [start, end] of ranges) {
49
+ if (start > cursor) parts.push({
50
+ text: message.slice(cursor, start),
51
+ match: false
52
+ });
53
+ parts.push({
54
+ text: message.slice(start, end),
55
+ match: true
56
+ });
57
+ cursor = end;
58
+ }
59
+ if (cursor < message.length) parts.push({
60
+ text: message.slice(cursor),
61
+ match: false
62
+ });
63
+ return parts;
64
+ }
65
+ function useLogStream({ lines, initialLevels }) {
66
+ const [query, setQuery] = useState("");
67
+ const [regex, setRegex] = useState(false);
68
+ const [levels, setLevels] = useState(initialLevels ?? /* @__PURE__ */ new Set());
69
+ const pattern = useMemo(() => compileQuery(query, regex), [query, regex]);
70
+ const invalidPattern = regex && query.length > 0 && pattern === null;
71
+ const visible = useMemo(() => {
72
+ const result = [];
73
+ for (const line of lines) {
74
+ if (levels.size > 0 && line.level && !levels.has(line.level)) continue;
75
+ const matches = findMatches(line.message, pattern);
76
+ if (pattern && matches.length === 0) continue;
77
+ result.push({
78
+ ...line,
79
+ matches
80
+ });
81
+ }
82
+ return result;
83
+ }, [
84
+ lines,
85
+ levels,
86
+ pattern
87
+ ]);
88
+ const counts = useMemo(() => {
89
+ const byLevel = /* @__PURE__ */ new Map();
90
+ for (const line of lines) {
91
+ if (!line.level) continue;
92
+ byLevel.set(line.level, (byLevel.get(line.level) ?? 0) + 1);
93
+ }
94
+ return byLevel;
95
+ }, [lines]);
96
+ return {
97
+ query,
98
+ setQuery,
99
+ regex,
100
+ setRegex,
101
+ levels,
102
+ toggleLevel: useCallback((level) => {
103
+ setLevels((current) => {
104
+ const next = new Set(current);
105
+ if (next.has(level)) next.delete(level);
106
+ else next.add(level);
107
+ return next;
108
+ });
109
+ }, []),
110
+ setLevels,
111
+ visible,
112
+ counts,
113
+ invalidPattern,
114
+ total: lines.length
115
+ };
116
+ }
117
+ //#endregion
118
+ export { LOG_LEVELS, compileQuery, findMatches, segment, useLogStream };
119
+
120
+ //# sourceMappingURL=log-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-stream.js","names":[],"sources":["../../../src/components/log-viewer/log-stream.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\n\n/**\n * Filtering and match-finding for a log stream.\n *\n * Separate from the view because it is pure and worth testing without a DOM,\n * and because the awkward parts are here rather than in the rendering: a bad\n * regex must not throw while someone is halfway through typing it, and a match\n * has to be located precisely enough to highlight rather than merely detected.\n */\n\nexport type LogLevel = \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\" | \"fatal\";\n\nexport const LOG_LEVELS: readonly LogLevel[] = [\n \"trace\",\n \"debug\",\n \"info\",\n \"warn\",\n \"error\",\n \"fatal\",\n];\n\nexport interface LogLine {\n id: string;\n message: string;\n level?: LogLevel;\n /** ISO timestamp, or anything the consumer wants shown in the gutter. */\n timestamp?: string;\n /** Structured fields, revealed when the row is expanded. */\n fields?: Record<string, unknown>;\n}\n\n/** A [start, end) slice of a message that matched the filter. */\nexport type MatchRange = readonly [number, number];\n\nexport interface FilterState {\n /** Substring, or a pattern when `regex` is on. */\n query: string;\n regex: boolean;\n /** Levels to show. Empty means all of them. */\n levels: Set<LogLevel>;\n}\n\n/**\n * Compiles the query.\n *\n * Returns null for an invalid pattern rather than throwing: the query is being\n * typed, so it spends most of its life syntactically incomplete, and a viewer\n * that crashes on \"(\" is unusable.\n */\nexport function compileQuery(query: string, regex: boolean): RegExp | null {\n if (query.length === 0) return null;\n try {\n return regex\n ? new RegExp(query, \"gi\")\n : new RegExp(query.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"), \"gi\");\n } catch {\n return null;\n }\n}\n\n/** Every match in a line, so each can be highlighted rather than just the first. */\nexport function findMatches(message: string, pattern: RegExp | null): MatchRange[] {\n if (!pattern) return [];\n\n const ranges: MatchRange[] = [];\n // Fresh lastIndex: the pattern is reused across lines and a global regex\n // carries position between calls, which would skip matches in later lines.\n pattern.lastIndex = 0;\n\n let match = pattern.exec(message);\n while (match !== null) {\n // A pattern that can match empty — \"a*\" — would loop forever otherwise.\n if (match[0].length === 0) {\n pattern.lastIndex += 1;\n } else {\n ranges.push([match.index, match.index + match[0].length]);\n }\n match = pattern.exec(message);\n }\n\n return ranges;\n}\n\n/** Splits a message into alternating plain and matched segments. */\nexport function segment(\n message: string,\n ranges: MatchRange[],\n): { text: string; match: boolean }[] {\n if (ranges.length === 0) return [{ text: message, match: false }];\n\n const parts: { text: string; match: boolean }[] = [];\n let cursor = 0;\n\n for (const [start, end] of ranges) {\n if (start > cursor) parts.push({ text: message.slice(cursor, start), match: false });\n parts.push({ text: message.slice(start, end), match: true });\n cursor = end;\n }\n if (cursor < message.length) parts.push({ text: message.slice(cursor), match: false });\n\n return parts;\n}\n\nexport interface VisibleLine extends LogLine {\n matches: MatchRange[];\n}\n\nexport interface UseLogStreamOptions {\n lines: LogLine[];\n /** Levels present but unchecked are hidden. Empty shows everything. */\n initialLevels?: Set<LogLevel>;\n}\n\nexport function useLogStream({ lines, initialLevels }: UseLogStreamOptions) {\n const [query, setQuery] = useState(\"\");\n const [regex, setRegex] = useState(false);\n const [levels, setLevels] = useState<Set<LogLevel>>(initialLevels ?? new Set());\n\n const pattern = useMemo(() => compileQuery(query, regex), [query, regex]);\n\n // An invalid pattern is reported, not swallowed: with no feedback the reader\n // sees an empty log and concludes nothing matched.\n const invalidPattern = regex && query.length > 0 && pattern === null;\n\n const visible = useMemo<VisibleLine[]>(() => {\n const result: VisibleLine[] = [];\n\n for (const line of lines) {\n if (levels.size > 0 && line.level && !levels.has(line.level)) continue;\n\n const matches = findMatches(line.message, pattern);\n // A query with no match hides the line; an invalid pattern hides nothing,\n // because the reader has not finished saying what they want yet.\n if (pattern && matches.length === 0) continue;\n\n result.push({ ...line, matches });\n }\n\n return result;\n }, [lines, levels, pattern]);\n\n const counts = useMemo(() => {\n const byLevel = new Map<LogLevel, number>();\n for (const line of lines) {\n if (!line.level) continue;\n byLevel.set(line.level, (byLevel.get(line.level) ?? 0) + 1);\n }\n return byLevel;\n }, [lines]);\n\n const toggleLevel = useCallback((level: LogLevel) => {\n setLevels((current) => {\n const next = new Set(current);\n if (next.has(level)) next.delete(level);\n else next.add(level);\n return next;\n });\n }, []);\n\n return {\n query,\n setQuery,\n regex,\n setRegex,\n levels,\n toggleLevel,\n setLevels,\n visible,\n counts,\n invalidPattern,\n total: lines.length,\n };\n}\n"],"mappings":";;;AAeA,MAAa,aAAkC;CAC7C;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;AA8BA,SAAgB,aAAa,OAAe,OAA+B;CACzE,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,IAAI;EACF,OAAO,QACH,IAAI,OAAO,OAAO,IAAI,IACtB,IAAI,OAAO,MAAM,QAAQ,uBAAuB,MAAM,GAAG,IAAI;CACnE,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,YAAY,SAAiB,SAAsC;CACjF,IAAI,CAAC,SAAS,OAAO,CAAC;CAEtB,MAAM,SAAuB,CAAC;CAG9B,QAAQ,YAAY;CAEpB,IAAI,QAAQ,QAAQ,KAAK,OAAO;CAChC,OAAO,UAAU,MAAM;EAErB,IAAI,MAAM,EAAE,CAAC,WAAW,GACtB,QAAQ,aAAa;OAErB,OAAO,KAAK,CAAC,MAAM,OAAO,MAAM,QAAQ,MAAM,EAAE,CAAC,MAAM,CAAC;EAE1D,QAAQ,QAAQ,KAAK,OAAO;CAC9B;CAEA,OAAO;AACT;;AAGA,SAAgB,QACd,SACA,QACoC;CACpC,IAAI,OAAO,WAAW,GAAG,OAAO,CAAC;EAAE,MAAM;EAAS,OAAO;CAAM,CAAC;CAEhE,MAAM,QAA4C,CAAC;CACnD,IAAI,SAAS;CAEb,KAAK,MAAM,CAAC,OAAO,QAAQ,QAAQ;EACjC,IAAI,QAAQ,QAAQ,MAAM,KAAK;GAAE,MAAM,QAAQ,MAAM,QAAQ,KAAK;GAAG,OAAO;EAAM,CAAC;EACnF,MAAM,KAAK;GAAE,MAAM,QAAQ,MAAM,OAAO,GAAG;GAAG,OAAO;EAAK,CAAC;EAC3D,SAAS;CACX;CACA,IAAI,SAAS,QAAQ,QAAQ,MAAM,KAAK;EAAE,MAAM,QAAQ,MAAM,MAAM;EAAG,OAAO;CAAM,CAAC;CAErF,OAAO;AACT;AAYA,SAAgB,aAAa,EAAE,OAAO,iBAAsC;CAC1E,MAAM,CAAC,OAAO,YAAY,SAAS,EAAE;CACrC,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,MAAM,CAAC,QAAQ,aAAa,SAAwB,iCAAiB,IAAI,IAAI,CAAC;CAE9E,MAAM,UAAU,cAAc,aAAa,OAAO,KAAK,GAAG,CAAC,OAAO,KAAK,CAAC;CAIxE,MAAM,iBAAiB,SAAS,MAAM,SAAS,KAAK,YAAY;CAEhE,MAAM,UAAU,cAA6B;EAC3C,MAAM,SAAwB,CAAC;EAE/B,KAAK,MAAM,QAAQ,OAAO;GACxB,IAAI,OAAO,OAAO,KAAK,KAAK,SAAS,CAAC,OAAO,IAAI,KAAK,KAAK,GAAG;GAE9D,MAAM,UAAU,YAAY,KAAK,SAAS,OAAO;GAGjD,IAAI,WAAW,QAAQ,WAAW,GAAG;GAErC,OAAO,KAAK;IAAE,GAAG;IAAM;GAAQ,CAAC;EAClC;EAEA,OAAO;CACT,GAAG;EAAC;EAAO;EAAQ;CAAO,CAAC;CAE3B,MAAM,SAAS,cAAc;EAC3B,MAAM,0BAAU,IAAI,IAAsB;EAC1C,KAAK,MAAM,QAAQ,OAAO;GACxB,IAAI,CAAC,KAAK,OAAO;GACjB,QAAQ,IAAI,KAAK,QAAQ,QAAQ,IAAI,KAAK,KAAK,KAAK,KAAK,CAAC;EAC5D;EACA,OAAO;CACT,GAAG,CAAC,KAAK,CAAC;CAWV,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,aAfkB,aAAa,UAAoB;GACnD,WAAW,YAAY;IACrB,MAAM,OAAO,IAAI,IAAI,OAAO;IAC5B,IAAI,KAAK,IAAI,KAAK,GAAG,KAAK,OAAO,KAAK;SACjC,KAAK,IAAI,KAAK;IACnB,OAAO;GACT,CAAC;EACH,GAAG,CAAC,CAQQ;EACV;EACA;EACA;EACA;EACA,OAAO,MAAM;CACf;AACF"}
@@ -0,0 +1,51 @@
1
+ import { LogLevel, VisibleLine } from "./log-stream.js";
2
+ import { ComponentPropsWithRef, ReactNode } from "react";
3
+ //#region src/components/log-viewer/log-viewer.d.ts
4
+ interface LogViewerProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
5
+ lines: VisibleLine[];
6
+ /** Names the region. */
7
+ label: string;
8
+ /** Row height in pixels. Rows are one line; expansion is measured. */
9
+ rowHeight?: number;
10
+ height?: number | string;
11
+ /**
12
+ * Read new lines aloud. Off by default — a console that announces every line
13
+ * makes a screen reader useless for anything else.
14
+ */
15
+ announce?: boolean;
16
+ /** The way out of the virtual window, for anyone who needs the whole log. */
17
+ onDownload?: () => void;
18
+ children?: ReactNode;
19
+ }
20
+ declare function LogViewer({ className, lines, label, rowHeight, height, announce, onDownload, children, ...props }: LogViewerProps): import("react").JSX.Element;
21
+ interface LogViewerRowProps {
22
+ line: VisibleLine;
23
+ expanded?: boolean;
24
+ onToggle?: () => void;
25
+ }
26
+ /**
27
+ * One line.
28
+ *
29
+ * Exported because the virtualized container cannot be measured outside a real
30
+ * browser — jsdom reports every box as zero — so the row's behaviour would
31
+ * otherwise be untestable. It is also genuinely useful on its own for anyone
32
+ * rendering a short log without virtualization.
33
+ */
34
+ declare function LogViewerRow({ line, expanded, onToggle }: LogViewerRowProps): import("react").JSX.Element;
35
+ interface LogViewerToolbarProps extends Omit<ComponentPropsWithRef<"div">, "onChange"> {
36
+ query: string;
37
+ onQueryChange: (query: string) => void;
38
+ regex: boolean;
39
+ onRegexChange: (regex: boolean) => void;
40
+ levels: Set<LogLevel>;
41
+ onToggleLevel: (level: LogLevel) => void;
42
+ counts: Map<LogLevel, number>;
43
+ invalidPattern?: boolean;
44
+ /** Shown as "N of M lines". */
45
+ showing: number;
46
+ total: number;
47
+ }
48
+ declare function LogViewerToolbar({ className, query, onQueryChange, regex, onRegexChange, levels, onToggleLevel, counts, invalidPattern, showing, total, ...props }: LogViewerToolbarProps): import("react").JSX.Element;
49
+ //#endregion
50
+ export { LogViewer, LogViewerProps, LogViewerRow, LogViewerRowProps, LogViewerToolbar, LogViewerToolbarProps };
51
+ //# sourceMappingURL=log-viewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-viewer.d.ts","names":[],"sources":["../../../src/components/log-viewer/log-viewer.tsx"],"mappings":";;;UAmDiB,uBAAuB,KAAK;EAC3C,OAAO;;EAEP;;EAEA;EACA;;;;;EAKA;;EAEA;EACA,WAAW;;iBAGG,YACd,WACA,OACA,OACA,WACA,QACA,UACA,YACA,aACG,SACF,iCAAc,IAAA;UAmJA;EACf,MAAM;EACN;EACA;;;;;;;;;;iBAWc,eAAe,MAAM,UAAkB,YAAY,oCAAiB,IAAA;UA2EnE,8BAA8B,KAAK;EAClD;EACA,gBAAgB;EAChB;EACA,gBAAgB;EAChB,QAAQ,IAAI;EACZ,gBAAgB,OAAO;EACvB,QAAQ,IAAI;EACZ;;EAEA;EACA;;iBAGc,mBACd,WACA,OACA,eACA,OACA,eACA,QACA,eACA,QACA,gBACA,SACA,UACG,SACF,wCAAqB,IAAA"}