@bro-code/crawl-space 0.0.0-stage → 2026.0.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +248 -2
  3. package/dist/cjs/api.js +90 -0
  4. package/dist/cjs/button.js +33 -0
  5. package/dist/cjs/context.js +72 -0
  6. package/dist/cjs/crawl-space.js +76 -0
  7. package/dist/cjs/css.js +104 -0
  8. package/dist/cjs/dialog.js +106 -0
  9. package/dist/cjs/dom.js +17 -0
  10. package/dist/cjs/host.js +2 -0
  11. package/dist/cjs/index.js +7 -0
  12. package/dist/cjs/lists.js +67 -0
  13. package/dist/cjs/options.js +80 -0
  14. package/dist/cjs/package.json +3 -0
  15. package/dist/cjs/sanitize.js +21 -0
  16. package/dist/cjs/styles.js +69 -0
  17. package/dist/cjs/submit.js +96 -0
  18. package/dist/cjs/themes.js +89 -0
  19. package/dist/cjs/types.js +2 -0
  20. package/dist/esm/api.js +86 -0
  21. package/dist/esm/button.js +30 -0
  22. package/dist/esm/context.js +68 -0
  23. package/dist/esm/crawl-space.js +73 -0
  24. package/dist/esm/css.js +101 -0
  25. package/dist/esm/dialog.js +103 -0
  26. package/dist/esm/dom.js +14 -0
  27. package/dist/esm/host.js +1 -0
  28. package/dist/esm/index.js +2 -0
  29. package/dist/esm/lists.js +63 -0
  30. package/dist/esm/options.js +76 -0
  31. package/dist/esm/package.json +3 -0
  32. package/dist/esm/sanitize.js +18 -0
  33. package/dist/esm/styles.js +65 -0
  34. package/dist/esm/submit.js +93 -0
  35. package/dist/esm/themes.js +85 -0
  36. package/dist/esm/types.js +1 -0
  37. package/dist/types/api.d.ts +20 -0
  38. package/dist/types/button.d.ts +8 -0
  39. package/dist/types/context.d.ts +15 -0
  40. package/dist/types/crawl-space.d.ts +11 -0
  41. package/dist/types/css.d.ts +2 -0
  42. package/dist/types/dialog.d.ts +13 -0
  43. package/dist/types/dom.d.ts +6 -0
  44. package/dist/types/host.d.ts +17 -0
  45. package/dist/types/index.d.ts +3 -0
  46. package/dist/types/lists.d.ts +9 -0
  47. package/dist/types/options.d.ts +23 -0
  48. package/dist/types/sanitize.d.ts +7 -0
  49. package/dist/types/styles.d.ts +14 -0
  50. package/dist/types/submit.d.ts +4 -0
  51. package/dist/types/themes.d.ts +5 -0
  52. package/dist/types/types.d.ts +112 -0
  53. package/package.json +60 -4
@@ -0,0 +1,93 @@
1
+ import { CONTEXT_LIMITS, collectContext } from './context.js';
2
+ import { h } from './dom.js';
3
+ import { sanitizeText } from './sanitize.js';
4
+ const PROMPTS = { bug: 'What went wrong?', question: 'What would you like to know?' };
5
+ export function createSubmitPanel(host) {
6
+ const bug = h('input', { type: 'radio', name: 'cs-type', value: 'bug', checked: true });
7
+ const question = h('input', { type: 'radio', name: 'cs-type', value: 'question' });
8
+ const prompt = h('label', { class: 'field-label', for: 'cs-description' });
9
+ const description = h('textarea', { id: 'cs-description', rows: '6' });
10
+ const counter = h('div', { class: 'counter' });
11
+ const include = h('input', { type: 'checkbox', checked: true });
12
+ const details = h('ul', { 'aria-label': 'Details sent with it' });
13
+ const detailsBox = h('div', { class: 'details' }, h('label', {}, include, 'Include these details'), details);
14
+ const message = h('p', { class: 'message', role: 'status' });
15
+ const send = h('button', { type: 'submit', class: 'primary' }, 'Send');
16
+ const form = h('form', {}, h('fieldset', { class: 'kinds' }, h('legend', { class: 'hidden-label' }, 'Type'), h('label', {}, bug, h('span', {}, 'Bug')), h('label', {}, question, h('span', {}, 'Question'))), prompt, description, counter, detailsBox, h('div', { class: 'actions' }, message, send));
17
+ let shown = [];
18
+ function say(kind, text) {
19
+ message.className = `message ${kind}`.trim();
20
+ message.textContent = text;
21
+ }
22
+ function update() {
23
+ prompt.textContent = question.checked ? PROMPTS.question : PROMPTS.bug;
24
+ counter.textContent = `${description.value.length} / ${host.options.maxLength}`;
25
+ details.classList.toggle('off', !include.checked);
26
+ for (const { field } of shown)
27
+ if (field)
28
+ field.disabled = !include.checked;
29
+ }
30
+ function contextToSend() {
31
+ const pairs = shown.flatMap(({ entry, field }) => {
32
+ const value = field ? sanitizeText(field.value, CONTEXT_LIMITS.maxValueLength) : entry.value;
33
+ return value === '' ? [] : [[entry.label, value]];
34
+ });
35
+ return pairs.length > 0 ? Object.fromEntries(pairs) : undefined;
36
+ }
37
+ form.addEventListener('change', update);
38
+ description.addEventListener('input', () => {
39
+ say('', '');
40
+ update();
41
+ });
42
+ form.addEventListener('submit', (event) => {
43
+ event.preventDefault();
44
+ const text = sanitizeText(description.value, host.options.maxLength);
45
+ if (text === '') {
46
+ say('error', 'Write something first.');
47
+ description.focus();
48
+ return;
49
+ }
50
+ const type = question.checked ? 'question' : 'bug';
51
+ const context = include.checked ? contextToSend() : undefined;
52
+ send.disabled = true;
53
+ say('', 'Sending...');
54
+ void host.api.submitReport({ type, description: text, ...(context && { context }) }).then((result) => {
55
+ send.disabled = false;
56
+ if (result.ok) {
57
+ form.reset();
58
+ update();
59
+ say('success', `Thanks! Your ${type === 'bug' ? 'report' : 'question'} was sent.`);
60
+ }
61
+ else if (result.reason === 'rate-limited') {
62
+ say('error', 'Too many sent in a short time. Please try again in a minute.');
63
+ }
64
+ else {
65
+ say('error', 'It could not be sent. Please try again.');
66
+ }
67
+ });
68
+ });
69
+ return {
70
+ element: form,
71
+ show() {
72
+ const before = new Map(shown.map((detail) => [detail.entry.label, detail]));
73
+ shown = collectContext(host.options).map((entry) => {
74
+ if (!entry.editable)
75
+ return { entry };
76
+ const field = h('input', { type: 'text', value: entry.value, maxlength: String(CONTEXT_LIMITS.maxValueLength) });
77
+ const previous = before.get(entry.label);
78
+ if (previous?.field && previous.entry.value === entry.value)
79
+ field.value = previous.field.value;
80
+ return { entry, field };
81
+ });
82
+ details.replaceChildren(...shown.map(({ entry, field }) => field
83
+ ? h('li', { class: 'editable' }, h('label', {}, h('span', { class: 'label' }, `${entry.label}: `), field))
84
+ : h('li', {}, h('span', { class: 'label' }, `${entry.label}: `), entry.value)));
85
+ detailsBox.hidden = shown.length === 0;
86
+ update();
87
+ },
88
+ refresh() {
89
+ description.maxLength = host.options.maxLength;
90
+ update();
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,85 @@
1
+ const DEFAULT = {
2
+ background: '#121417',
3
+ surface: '#1c1f24',
4
+ text: '#e8eaed',
5
+ muted: '#9aa0a6',
6
+ border: '#33373d',
7
+ accent: '#5b9cff',
8
+ accentText: '#0b1220',
9
+ error: '#ff6b6b',
10
+ success: '#6fcf97',
11
+ font: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
12
+ };
13
+ const BROCODE = {
14
+ background: '#181818',
15
+ surface: '#222222',
16
+ text: '#ffffff',
17
+ muted: '#b0b0b0',
18
+ border: '#3c3c3c',
19
+ accent: '#7b9981',
20
+ accentText: '#ffffff',
21
+ error: '#ff5252',
22
+ success: '#96be4d',
23
+ font: 'Roboto, sans-serif',
24
+ };
25
+ const ROMBIE = {
26
+ background: '#0a0a12',
27
+ surface: '#15141f',
28
+ text: '#f1eef9',
29
+ muted: '#9a90bd',
30
+ border: '#302c47',
31
+ accent: '#ff2d78',
32
+ accentText: '#12060d',
33
+ error: '#ff5c5c',
34
+ success: '#00e5ff',
35
+ font: "system-ui, 'Segoe UI', Roboto, sans-serif",
36
+ };
37
+ export const THEMES = {
38
+ default: DEFAULT,
39
+ brocode: BROCODE,
40
+ rombie: ROMBIE,
41
+ 'taut-wad': BROCODE,
42
+ custom: DEFAULT,
43
+ };
44
+ const COLOR_KEYS = Object.keys(DEFAULT);
45
+ function isTheme(name) {
46
+ return Object.hasOwn(THEMES, name);
47
+ }
48
+ function isValid(key, value) {
49
+ if (typeof value !== 'string' || value.trim() === '')
50
+ return false;
51
+ return CSS.supports(key === 'font' ? 'font-family' : 'color', value);
52
+ }
53
+ export function resolveColors(config) {
54
+ const theme = config.theme ?? 'default';
55
+ if (typeof theme !== 'string' || !isTheme(theme)) {
56
+ console.warn(`[crawl-space] Unknown theme "${String(theme)}"; using "default".`);
57
+ return { ...DEFAULT };
58
+ }
59
+ if (theme !== 'custom') {
60
+ if (config.colors !== undefined) {
61
+ console.warn(`[crawl-space] "colors" is only used with theme "custom"; ignoring it.`);
62
+ }
63
+ return { ...THEMES[theme] };
64
+ }
65
+ const colors = { ...DEFAULT };
66
+ const given = config.colors ?? {};
67
+ if (typeof given !== 'object' || given === null || Array.isArray(given)) {
68
+ console.warn('[crawl-space] "colors" must be an object; using the default theme.');
69
+ return colors;
70
+ }
71
+ for (const [key, value] of Object.entries(given)) {
72
+ if (value === undefined)
73
+ continue;
74
+ if (!COLOR_KEYS.includes(key)) {
75
+ console.warn(`[crawl-space] Unknown color "${key}"; ignoring it.`);
76
+ }
77
+ else if (isValid(key, value)) {
78
+ colors[key] = value;
79
+ }
80
+ else {
81
+ console.warn(`[crawl-space] Invalid ${key} "${String(value)}"; using the default theme's.`);
82
+ }
83
+ }
84
+ return colors;
85
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { CrawlSpaceOptions, CrawlSpaceQaItem, CrawlSpaceReports, CrawlSpaceSubmission } from './types.js';
2
+ /** `rate-limited` is a 429; `failed` is anything else: the network, another status or a malformed body. */
3
+ export type ApiResult<T> = {
4
+ ok: true;
5
+ value: T;
6
+ } | {
7
+ ok: false;
8
+ reason: 'rate-limited' | 'failed';
9
+ };
10
+ export interface CrawlSpaceApi {
11
+ submitReport(submission: CrawlSpaceSubmission): Promise<ApiResult<{
12
+ id: number | string;
13
+ }>>;
14
+ getReports(): Promise<ApiResult<CrawlSpaceReports>>;
15
+ getQa(): Promise<ApiResult<CrawlSpaceQaItem[]>>;
16
+ }
17
+ /** Joins a base URL and a path with exactly one `/` between them. */
18
+ export declare function joinUrl(base: string, path: string): string;
19
+ /** A client for the crawl-space HTTP contract. Its calls resolve to an `ApiResult` and never throw. */
20
+ export declare function createApiClient(options: Pick<CrawlSpaceOptions, 'api' | 'paths' | 'getToken'>): CrawlSpaceApi;
@@ -0,0 +1,8 @@
1
+ import type { Host } from './host.js';
2
+ /**
3
+ * `<crawl-space-button tab="active">` opens the dialog at `tab` (default
4
+ * `submit`). Its label is its text, or the title when it has none. With
5
+ * `floating` it is fixed in a corner, at `floating.position`. The inner button
6
+ * is `::part(button)`.
7
+ */
8
+ export declare function defineButton(getHost: () => Host): CustomElementConstructor;
@@ -0,0 +1,15 @@
1
+ import type { CrawlSpaceContextEntry, CrawlSpaceOptions } from './types.js';
2
+ /** What brocodetech accepts in a report's context; it refuses more. */
3
+ export declare const CONTEXT_LIMITS: {
4
+ readonly maxEntries: 12;
5
+ readonly maxKeyLength: 40;
6
+ readonly maxValueLength: 200;
7
+ };
8
+ /**
9
+ * The details sent with a report, in the order shown: Version, Page (the path
10
+ * only, through `scrubPath`), Browser, OS, then `getContext()`'s entries, which
11
+ * alone can be editable. Labels and values are sanitized and capped; an empty
12
+ * one, a non-string value or a repeated label is left out, and at most
13
+ * `CONTEXT_LIMITS.maxEntries` are kept.
14
+ */
15
+ export declare function collectContext(options: Pick<CrawlSpaceOptions, 'version' | 'scrubPath' | 'getContext'>): CrawlSpaceContextEntry[];
@@ -0,0 +1,11 @@
1
+ import type { CrawlSpaceController, CrawlSpaceOptions } from './types.js';
2
+ /**
3
+ * Sets up the help desk: defines `<crawl-space-button>` and the dialog, adds
4
+ * the floating button when `floating` is set, and returns `open(tab?)` and
5
+ * `close()`. Call it once; a second call replaces the options and returns the
6
+ * same controller. Without a `window` it does nothing and returns a
7
+ * controller whose methods do nothing.
8
+ *
9
+ * @throws TypeError when `api` is not a non-empty string.
10
+ */
11
+ export declare function createCrawlSpace(options: CrawlSpaceOptions): CrawlSpaceController;
@@ -0,0 +1,2 @@
1
+ export declare const BUTTON_CSS = "\n:host { display: inline-block; }\n:host([hidden]) { display: none; }\n:host([floating]) { position: fixed; z-index: 2147483000; }\n:host([floating][data-position='bottom-right']) { right: 16px; bottom: 16px; }\n:host([floating][data-position='bottom-left']) { left: 16px; bottom: 16px; }\n:host([floating][data-position='top-right']) { right: 16px; top: 16px; }\n:host([floating][data-position='top-left']) { left: 16px; top: 16px; }\nbutton {\n padding: 9px 16px;\n border: none;\n border-radius: 8px;\n background: var(--cs-accent);\n color: var(--cs-accent-text);\n font: 600 14px/1.2 var(--cs-font);\n cursor: pointer;\n}\n:host([floating]) button { padding: 11px 18px; border-radius: 999px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4); }\nbutton:hover { filter: brightness(1.1); }\nbutton:focus-visible { outline: 2px solid var(--cs-text); outline-offset: 2px; }\n";
2
+ export declare const DIALOG_CSS = "\n:host { all: initial; }\n*, *::before, *::after { box-sizing: border-box; }\ndialog {\n width: min(560px, calc(100vw - 32px));\n max-height: min(720px, calc(100vh - 32px));\n padding: 0;\n border: 1px solid var(--cs-border);\n border-radius: 12px;\n background: var(--cs-background);\n color: var(--cs-text);\n font: 14px/1.45 var(--cs-font);\n box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);\n overflow: hidden;\n}\ndialog[open] { display: flex; flex-direction: column; }\ndialog::backdrop { background: rgba(0, 0, 0, 0.6); }\nbutton, input, textarea { font: inherit; color: inherit; }\nbutton { cursor: pointer; }\nbutton:disabled { cursor: default; opacity: 0.6; }\n:focus-visible { outline: 2px solid var(--cs-accent); outline-offset: 2px; }\nheader { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 12px 0 20px; }\nh2 { margin: 0; font-size: 18px; font-weight: 600; }\n.close { display: grid; place-items: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: 8px; background: none; color: var(--cs-muted); }\n.close:hover { background: var(--cs-surface); color: var(--cs-text); }\n.close svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }\n[role='tablist'] { display: flex; gap: 4px; padding: 8px 14px 0; border-bottom: 1px solid var(--cs-border); }\n[role='tab'] { margin-bottom: -1px; padding: 9px 10px; border: none; border-bottom: 2px solid transparent; background: none; color: var(--cs-muted); font-weight: 600; }\n[role='tab']:hover { color: var(--cs-text); }\n[role='tab'][aria-selected='true'] { border-bottom-color: var(--cs-accent); color: var(--cs-text); }\n[role='tabpanel'] { flex: 1 1 auto; min-height: 0; padding: 16px 20px 20px; overflow-y: auto; }\n[role='tabpanel'][hidden] { display: none; }\n.hidden-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n.kinds { display: inline-flex; gap: 3px; margin: 0 0 14px; padding: 3px; border: 1px solid var(--cs-border); border-radius: 8px; }\n.kinds label { position: relative; }\n.kinds input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }\n.kinds span { display: block; padding: 5px 14px; border-radius: 6px; color: var(--cs-muted); font-weight: 600; }\n.kinds input:checked + span { background: var(--cs-accent); color: var(--cs-accent-text); }\n.kinds input:focus-visible + span { outline: 2px solid var(--cs-accent); outline-offset: 2px; }\n.field-label { display: block; margin-bottom: 6px; font-weight: 600; }\ntextarea { display: block; width: 100%; min-height: 130px; padding: 10px 12px; border: 1px solid var(--cs-border); border-radius: 8px; background: var(--cs-surface); resize: vertical; }\ntextarea:focus-visible { outline-offset: 0; }\n.counter { margin-top: 4px; color: var(--cs-muted); font-size: 12px; text-align: right; }\n.details { margin-top: 12px; padding: 10px 12px; border: 1px solid var(--cs-border); border-radius: 8px; background: var(--cs-surface); }\n.details label { display: flex; align-items: center; gap: 8px; font-weight: 600; cursor: pointer; }\n.details input { width: 16px; height: 16px; margin: 0; accent-color: var(--cs-accent); }\n.details ul { margin: 8px 0 0; padding: 0; list-style: none; color: var(--cs-muted); font-size: 12px; }\n.details ul.off { opacity: 0.45; }\n.details li { overflow-wrap: anywhere; }\n.details .label { color: var(--cs-text); }\n.details li.editable { margin: 2px 0; }\n.details li.editable label { gap: 4px; font-weight: inherit; cursor: auto; }\n.details li.editable .label { flex: none; }\n.details li.editable input { flex: 1 1 auto; min-width: 0; width: auto; height: auto; padding: 2px 6px; border: 1px solid var(--cs-border); border-radius: 4px; background: var(--cs-background); color: var(--cs-text); font: inherit; }\n.details li.editable input:focus-visible { outline: 2px solid var(--cs-accent); outline-offset: 1px; }\n.details li.editable input:disabled { cursor: not-allowed; }\n.actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 16px; }\n.message { flex: 1; margin: 0; font-size: 13px; }\n.error { color: var(--cs-error); }\n.success { color: var(--cs-success); }\n.primary { padding: 9px 18px; border: none; border-radius: 8px; background: var(--cs-accent); color: var(--cs-accent-text); font-weight: 600; }\n.primary:hover:not(:disabled) { filter: brightness(1.1); }\n.secondary { padding: 6px 12px; border: 1px solid var(--cs-border); border-radius: 8px; background: none; }\nsection + section { margin-top: 18px; }\nh3 { margin: 0 0 8px; color: var(--cs-muted); font-size: 12px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; }\n.list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }\n.item { padding: 10px 12px; border: 1px solid var(--cs-border); border-radius: 8px; background: var(--cs-surface); }\n.meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-bottom: 6px; color: var(--cs-muted); font-size: 12px; }\n.badge { padding: 1px 8px; border: 1px solid var(--cs-border); border-radius: 999px; color: var(--cs-text); }\n.badge.in_progress { border-color: var(--cs-accent); color: var(--cs-accent); }\n.badge.resolved { border-color: var(--cs-success); color: var(--cs-success); }\n.text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }\n.answer { margin-top: 8px; padding-left: 10px; border-left: 2px solid var(--cs-accent); }\n.answer-label { color: var(--cs-muted); font-size: 12px; font-weight: 600; }\n.note { margin: 0; color: var(--cs-muted); }\n.failed { display: flex; align-items: center; gap: 12px; }\ndetails { border: 1px solid var(--cs-border); border-radius: 8px; background: var(--cs-surface); }\nsummary { padding: 10px 12px; font-weight: 600; cursor: pointer; }\ndetails .text { padding: 0 12px 12px; }\n";
@@ -0,0 +1,13 @@
1
+ import type { Host } from './host.js';
2
+ import type { CrawlSpaceTab } from './types.js';
3
+ export interface DialogElement extends HTMLElement {
4
+ /** Opens at `tab`, or switches to it when already open. */
5
+ showTab(tab: CrawlSpaceTab): void;
6
+ hide(): void;
7
+ }
8
+ /**
9
+ * `<crawl-space-dialog>`, added to `document.body` on first open. A native
10
+ * modal `<dialog>` gives the top layer, Escape to close, a focus trap and focus
11
+ * back to whatever opened it.
12
+ */
13
+ export declare function defineDialog(getHost: () => Host): CustomElementConstructor;
@@ -0,0 +1,6 @@
1
+ export type Child = Node | string | null | undefined | false;
2
+ /**
3
+ * Creates an element with attributes (`true` sets an empty one, `false` none)
4
+ * and children. Strings become text nodes, never markup.
5
+ */
6
+ export declare function h<K extends keyof HTMLElementTagNameMap>(tag: K, attributes?: Record<string, string | boolean>, ...children: Child[]): HTMLElementTagNameMap[K];
@@ -0,0 +1,17 @@
1
+ import type { CrawlSpaceApi } from './api.js';
2
+ import type { ResolvedOptions } from './options.js';
3
+ import type { Styles } from './styles.js';
4
+ import type { CrawlSpaceTab } from './types.js';
5
+ export interface Refreshable {
6
+ /** Picks up replaced options: title, theme, limits. */
7
+ refresh(): void;
8
+ }
9
+ /** What the elements share with the controller. A second `createCrawlSpace` replaces `options` and `api` in place. */
10
+ export interface Host {
11
+ readonly options: ResolvedOptions;
12
+ readonly api: CrawlSpaceApi;
13
+ readonly styles: Styles;
14
+ /** The elements in the document, refreshed when the options are replaced. */
15
+ readonly connected: Set<Refreshable>;
16
+ open(tab: CrawlSpaceTab): void;
17
+ }
@@ -0,0 +1,3 @@
1
+ export type { CrawlSpaceColors, CrawlSpaceConfig, CrawlSpaceContextEntry, CrawlSpaceController, CrawlSpaceDetail, CrawlSpaceOptions, CrawlSpacePosition, CrawlSpaceQaItem, CrawlSpaceReport, CrawlSpaceReportStatus, CrawlSpaceReportType, CrawlSpaceReports, CrawlSpaceSubmission, CrawlSpaceTab, CrawlSpaceTheme, } from './types.js';
2
+ export { createCrawlSpace } from './crawl-space.js';
3
+ export { sanitizeText } from './sanitize.js';
@@ -0,0 +1,9 @@
1
+ import type { Host } from './host.js';
2
+ /** A tab's content. `show` runs each time the tab is shown. */
3
+ export interface Panel {
4
+ readonly element: HTMLElement;
5
+ show(): void;
6
+ refresh?(): void;
7
+ }
8
+ export declare function createActivePanel(host: Host): Panel;
9
+ export declare function createQaPanel(host: Host): Panel;
@@ -0,0 +1,23 @@
1
+ import type { CrawlSpaceColors, CrawlSpaceOptions, CrawlSpacePosition, CrawlSpaceTab } from './types.js';
2
+ /** The options after checking, with every default filled in. */
3
+ export interface ResolvedOptions {
4
+ api: string;
5
+ paths: {
6
+ reports: string;
7
+ qa: string;
8
+ };
9
+ colors: CrawlSpaceColors;
10
+ /** Where the floating button goes; null for none. */
11
+ floating: CrawlSpacePosition | null;
12
+ title: string;
13
+ maxLength: number;
14
+ getToken?: CrawlSpaceOptions['getToken'];
15
+ version?: string;
16
+ scrubPath?: CrawlSpaceOptions['scrubPath'];
17
+ getContext?: CrawlSpaceOptions['getContext'];
18
+ onOpenChange?: CrawlSpaceOptions['onOpenChange'];
19
+ }
20
+ /** A tab name, or `submit` (with a warning for anything but nothing) when it isn't one. */
21
+ export declare function toTab(value: unknown): CrawlSpaceTab;
22
+ /** Checks the options at the package's boundary: `api` must be set; anything else invalid falls back with a warning. */
23
+ export declare function resolveOptions(options: CrawlSpaceOptions): ResolvedOptions;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Cleans text a user typed before it is sent: NFC, no control, zero-width or
3
+ * bidi characters, `\n` line ends, runs of three or more blank lines collapsed
4
+ * to one, trimmed, and at most `maxLength` UTF-16 units (never ending in half
5
+ * of a surrogate pair). The server stays the authority on what it accepts.
6
+ */
7
+ export declare function sanitizeText(text: string, maxLength: number): string;
@@ -0,0 +1,14 @@
1
+ import type { CrawlSpaceColors } from './types.js';
2
+ /**
3
+ * The theme as custom properties in a `:host` rule. A page's own rules beat
4
+ * `:host`, so `crawl-space-button { --cs-accent: ... }` overrides it.
5
+ */
6
+ export declare function themeCss(colors: CrawlSpaceColors): string;
7
+ export interface Styles {
8
+ /** Gives a shadow root `css` and the current theme. Safe to call again. */
9
+ apply(root: ShadowRoot, css: string): void;
10
+ /** Replaces the theme; roots without constructable sheets pick it up on their next `apply`. */
11
+ setColors(colors: CrawlSpaceColors): void;
12
+ }
13
+ /** One theme shared by every element: a constructable stylesheet where supported, else a `<style>` in each root. */
14
+ export declare function createStyles(colors: CrawlSpaceColors): Styles;
@@ -0,0 +1,4 @@
1
+ import type { Host } from './host.js';
2
+ import type { Panel } from './lists.js';
3
+ /** The Submit tab: a bug or a question, and the details that go with it when ticked. */
4
+ export declare function createSubmitPanel(host: Host): Panel;
@@ -0,0 +1,5 @@
1
+ import type { CrawlSpaceColors, CrawlSpaceConfig, CrawlSpaceTheme } from './types.js';
2
+ /** Each preset's colors. `custom` is the base that `colors` go over. */
3
+ export declare const THEMES: Readonly<Record<CrawlSpaceTheme, Readonly<CrawlSpaceColors>>>;
4
+ /** The colors for a config's `theme`, with `colors` over `default` for `custom`. */
5
+ export declare function resolveColors(config: Pick<CrawlSpaceConfig, 'theme' | 'colors'>): CrawlSpaceColors;
@@ -0,0 +1,112 @@
1
+ /** The preset themes. `custom` is `colors` over `default`. */
2
+ export type CrawlSpaceTheme = 'default' | 'brocode' | 'rombie' | 'taut-wad' | 'custom';
3
+ export type CrawlSpacePosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
4
+ /** Every value is a CSS color, except `font`, which is a CSS `font-family`. */
5
+ export interface CrawlSpaceColors {
6
+ background: string;
7
+ surface: string;
8
+ text: string;
9
+ muted: string;
10
+ border: string;
11
+ accent: string;
12
+ /** Text on an `accent` background. */
13
+ accentText: string;
14
+ error: string;
15
+ success: string;
16
+ font: string;
17
+ }
18
+ /**
19
+ * The JSON-safe settings, kept in the app's `bro.config.json` and passed in.
20
+ * `theme` and `position` also take a plain `string`, because a JSON import types
21
+ * them that way; an unknown name falls back to the default with a warning.
22
+ */
23
+ export interface CrawlSpaceConfig {
24
+ /** Base URL of the API, e.g. `https://api.example.com/api/crawl-space/my-app`. */
25
+ api: string;
26
+ /** Route paths under `api`. Default: `/reports` and `/qa`. */
27
+ paths?: {
28
+ reports?: string;
29
+ qa?: string;
30
+ };
31
+ /** Default: `default`. */
32
+ theme?: CrawlSpaceTheme | (string & {});
33
+ /** Used with `theme: 'custom'`; a missing or invalid value falls back to `default`'s. */
34
+ colors?: Partial<CrawlSpaceColors>;
35
+ /** Adds a floating button at `position` (default `bottom-right`). */
36
+ floating?: {
37
+ position?: CrawlSpacePosition | (string & {});
38
+ };
39
+ /** Dialog title and default button label. Default: `Help & bug reports`. */
40
+ title?: string;
41
+ /** Longest description accepted. Default: 2000. */
42
+ maxLength?: number;
43
+ }
44
+ /** The config plus the parts only code can supply. */
45
+ export interface CrawlSpaceOptions extends CrawlSpaceConfig {
46
+ /** The signed-in user's token, sent as `Authorization: Bearer <token>`; null when signed out. */
47
+ getToken?: () => string | null | Promise<string | null>;
48
+ /** The app's version, sent as context. */
49
+ version?: string;
50
+ /** Removes anything private from the page path before it is shown or sent. */
51
+ scrubPath?: (path: string) => string;
52
+ /**
53
+ * More context entries, label to value, shown and sent after the built-in ones. A string is shown as it is; a
54
+ * `CrawlSpaceDetail` with `editable: true` is shown in a field the user can change or clear.
55
+ */
56
+ getContext?: () => Record<string, string | CrawlSpaceDetail>;
57
+ /** Called with true when the dialog opens and false when it closes. */
58
+ onOpenChange?: (open: boolean) => void;
59
+ }
60
+ export type CrawlSpaceTab = 'submit' | 'active' | 'qa';
61
+ /** What `createCrawlSpace` returns. */
62
+ export interface CrawlSpaceController {
63
+ /** Opens the dialog at `tab` (default `submit`), or switches to it if already open. */
64
+ open(tab?: CrawlSpaceTab): void;
65
+ close(): void;
66
+ }
67
+ /** A detail from `getContext`, with whether the user can change it before sending. */
68
+ export interface CrawlSpaceDetail {
69
+ value: string;
70
+ editable?: boolean;
71
+ }
72
+ /** One line of the details sent with a report. */
73
+ export interface CrawlSpaceContextEntry {
74
+ label: string;
75
+ value: string;
76
+ editable: boolean;
77
+ }
78
+ export type CrawlSpaceReportType = 'bug' | 'question';
79
+ export type CrawlSpaceReportStatus = 'new' | 'in_progress' | 'resolved' | 'dismissed';
80
+ /** The body of `POST {api}{paths.reports}`. */
81
+ export interface CrawlSpaceSubmission {
82
+ type: CrawlSpaceReportType;
83
+ description: string;
84
+ /** Label to value; left out when the user unticks the details. */
85
+ context?: Record<string, string>;
86
+ }
87
+ export interface CrawlSpaceReport {
88
+ id: number | string;
89
+ type: CrawlSpaceReportType;
90
+ status: CrawlSpaceReportStatus;
91
+ description: string;
92
+ /** The developer's answer; only ever sent for the user's own reports. */
93
+ answer: string | null;
94
+ /** ISO 8601. */
95
+ createdAt: string;
96
+ /** ISO 8601. */
97
+ updatedAt: string;
98
+ }
99
+ export interface CrawlSpaceQaItem {
100
+ id: number | string;
101
+ question: string;
102
+ answer: string;
103
+ /** ISO 8601. */
104
+ updatedAt: string;
105
+ }
106
+ /** The body of `GET {api}{paths.reports}`. */
107
+ export interface CrawlSpaceReports {
108
+ /** The signed-in user's reports in every status; empty when signed out. */
109
+ mine: CrawlSpaceReport[];
110
+ /** Everyone else's reports being worked on. */
111
+ active: CrawlSpaceReport[];
112
+ }
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@bro-code/crawl-space",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2026.0.0",
4
+ "type": "module",
5
+ "description": "A help desk for any web app (bug reports, active issues, Q&A) as framework-free custom elements, themed, against a documented HTTP API.",
6
+ "main": "./dist/cjs/index.js",
7
+ "module": "./dist/esm/index.js",
8
+ "types": "./dist/types/index.d.ts",
9
+ "sideEffects": false,
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/types/index.d.ts",
13
+ "import": "./dist/esm/index.js",
14
+ "require": "./dist/cjs/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "npm run build:esm && npm run build:cjs && npm run build:types && npm run build:fixup",
24
+ "build:esm": "tsc --project tsconfig.esm.json",
25
+ "build:cjs": "tsc --project tsconfig.cjs.json",
26
+ "build:types": "tsc --project tsconfig.types.json",
27
+ "build:fixup": "node scripts/fixup.cjs",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "lint": "eslint src/",
31
+ "prepublishOnly": "npm run lint && npm run test && npm run build",
32
+ "preversion": "npm run lint && npm run test",
33
+ "version": "git add -A"
34
+ },
35
+ "keywords": [
36
+ "help-desk",
37
+ "bug-report",
38
+ "feedback",
39
+ "custom-elements",
40
+ "web-components",
41
+ "browser",
42
+ "bro-code"
43
+ ],
44
+ "author": "Bro Code Technologies",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/Bro-Code-Technologies/bro-code-package-maker.git",
49
+ "directory": "packages/crawl-space"
50
+ },
51
+ "engines": {
52
+ "node": ">=18"
53
+ },
54
+ "devDependencies": {
55
+ "@eslint/js": "~10.0.1",
56
+ "eslint": "~10.13.0",
57
+ "happy-dom": "~21.0.4",
58
+ "typescript": "~6.0.3",
59
+ "typescript-eslint": "~8.72.1",
60
+ "vitest": "~4.1.12"
61
+ }
62
+ }