@bro-code/crawl-space 0.0.0-stage → 2026.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -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 +121 -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 +118 -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,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DIALOG_CSS = exports.BUTTON_CSS = void 0;
4
+ exports.BUTTON_CSS = `
5
+ :host { display: inline-block; }
6
+ :host([hidden]) { display: none; }
7
+ :host([floating]) { position: fixed; z-index: 2147483000; }
8
+ :host([floating][data-position='bottom-right']) { right: 16px; bottom: 16px; }
9
+ :host([floating][data-position='bottom-left']) { left: 16px; bottom: 16px; }
10
+ :host([floating][data-position='top-right']) { right: 16px; top: 16px; }
11
+ :host([floating][data-position='top-left']) { left: 16px; top: 16px; }
12
+ button {
13
+ padding: 9px 16px;
14
+ border: none;
15
+ border-radius: 8px;
16
+ background: var(--cs-accent);
17
+ color: var(--cs-accent-text);
18
+ font: 600 14px/1.2 var(--cs-font);
19
+ cursor: pointer;
20
+ }
21
+ :host([floating]) button { padding: 11px 18px; border-radius: 999px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4); }
22
+ button:hover { filter: brightness(1.1); }
23
+ button:focus-visible { outline: 2px solid var(--cs-text); outline-offset: 2px; }
24
+ `;
25
+ exports.DIALOG_CSS = `
26
+ :host { all: initial; }
27
+ *, *::before, *::after { box-sizing: border-box; }
28
+ dialog {
29
+ width: min(560px, calc(100vw - 32px));
30
+ max-height: min(720px, calc(100vh - 32px));
31
+ padding: 0;
32
+ border: 1px solid var(--cs-border);
33
+ border-radius: 12px;
34
+ background: var(--cs-background);
35
+ color: var(--cs-text);
36
+ font: 14px/1.45 var(--cs-font);
37
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
38
+ overflow: hidden;
39
+ }
40
+ dialog[open] { display: flex; flex-direction: column; }
41
+ dialog::backdrop { background: rgba(0, 0, 0, 0.6); }
42
+ button, input, textarea { font: inherit; color: inherit; }
43
+ button { cursor: pointer; }
44
+ button:disabled { cursor: default; opacity: 0.6; }
45
+ :focus-visible { outline: 2px solid var(--cs-accent); outline-offset: 2px; }
46
+ header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 12px 0 20px; }
47
+ h2 { margin: 0; font-size: 18px; font-weight: 600; }
48
+ .close { display: grid; place-items: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: 8px; background: none; color: var(--cs-muted); }
49
+ .close:hover { background: var(--cs-surface); color: var(--cs-text); }
50
+ .close svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
51
+ [role='tablist'] { display: flex; gap: 4px; padding: 8px 14px 0; border-bottom: 1px solid var(--cs-border); }
52
+ [role='tab'] { margin-bottom: -1px; padding: 9px 10px; border: none; border-bottom: 2px solid transparent; background: none; color: var(--cs-muted); font-weight: 600; }
53
+ [role='tab']:hover { color: var(--cs-text); }
54
+ [role='tab'][aria-selected='true'] { border-bottom-color: var(--cs-accent); color: var(--cs-text); }
55
+ [role='tabpanel'] { flex: 1 1 auto; min-height: 0; padding: 16px 20px 20px; overflow-y: auto; }
56
+ [role='tabpanel'][hidden] { display: none; }
57
+ .hidden-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
58
+ .kinds { display: inline-flex; gap: 3px; margin: 0 0 14px; padding: 3px; border: 1px solid var(--cs-border); border-radius: 8px; }
59
+ .kinds label { position: relative; }
60
+ .kinds input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
61
+ .kinds span { display: block; padding: 5px 14px; border-radius: 6px; color: var(--cs-muted); font-weight: 600; }
62
+ .kinds input:checked + span { background: var(--cs-accent); color: var(--cs-accent-text); }
63
+ .kinds input:focus-visible + span { outline: 2px solid var(--cs-accent); outline-offset: 2px; }
64
+ .field-label { display: block; margin-bottom: 6px; font-weight: 600; }
65
+ textarea { 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; }
66
+ textarea:focus-visible { outline-offset: 0; }
67
+ .counter { margin-top: 4px; color: var(--cs-muted); font-size: 12px; text-align: right; }
68
+ .details { margin-top: 12px; padding: 10px 12px; border: 1px solid var(--cs-border); border-radius: 8px; background: var(--cs-surface); }
69
+ .details label { display: flex; align-items: center; gap: 8px; font-weight: 600; cursor: pointer; }
70
+ .details input { width: 16px; height: 16px; margin: 0; accent-color: var(--cs-accent); }
71
+ .details ul { margin: 8px 0 0; padding: 0; list-style: none; color: var(--cs-muted); font-size: 12px; }
72
+ .details ul.off { opacity: 0.45; }
73
+ .details li { overflow-wrap: anywhere; }
74
+ .details .label { color: var(--cs-text); }
75
+ .details li.editable { margin: 2px 0; }
76
+ .details li.editable label { gap: 4px; font-weight: inherit; cursor: auto; }
77
+ .details li.editable .label { flex: none; }
78
+ .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; }
79
+ .details li.editable input:focus-visible { outline: 2px solid var(--cs-accent); outline-offset: 1px; }
80
+ .details li.editable input:disabled { cursor: not-allowed; }
81
+ .actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 16px; }
82
+ .message { flex: 1; margin: 0; font-size: 13px; }
83
+ .error { color: var(--cs-error); }
84
+ .success { color: var(--cs-success); }
85
+ .primary { padding: 9px 18px; border: none; border-radius: 8px; background: var(--cs-accent); color: var(--cs-accent-text); font-weight: 600; }
86
+ .primary:hover:not(:disabled) { filter: brightness(1.1); }
87
+ .secondary { padding: 6px 12px; border: 1px solid var(--cs-border); border-radius: 8px; background: none; }
88
+ section + section { margin-top: 18px; }
89
+ h3 { margin: 0 0 8px; color: var(--cs-muted); font-size: 12px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; }
90
+ .list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
91
+ .item { padding: 10px 12px; border: 1px solid var(--cs-border); border-radius: 8px; background: var(--cs-surface); }
92
+ .meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-bottom: 6px; color: var(--cs-muted); font-size: 12px; }
93
+ .badge { padding: 1px 8px; border: 1px solid var(--cs-border); border-radius: 999px; color: var(--cs-text); }
94
+ .badge.in_progress { border-color: var(--cs-accent); color: var(--cs-accent); }
95
+ .badge.resolved { border-color: var(--cs-success); color: var(--cs-success); }
96
+ .text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
97
+ .answer { margin-top: 8px; padding-left: 10px; border-left: 2px solid var(--cs-accent); }
98
+ .answer-label { color: var(--cs-muted); font-size: 12px; font-weight: 600; }
99
+ .note { margin: 0; color: var(--cs-muted); }
100
+ .failed { display: flex; align-items: center; gap: 12px; }
101
+ details { border: 1px solid var(--cs-border); border-radius: 8px; background: var(--cs-surface); }
102
+ summary { padding: 10px 12px; font-weight: 600; cursor: pointer; }
103
+ details .text { padding: 0 12px 12px; }
104
+ @media (max-width: 599.98px) {
105
+ dialog { width: 100%; max-width: none; height: 100vh; height: 100dvh; max-height: none; margin: 0; border: none; border-radius: 0; box-shadow: none; }
106
+ }
107
+ @media (hover: none) {
108
+ header { padding: 8px 8px 0 20px; }
109
+ .close { width: 48px; height: 48px; }
110
+ .close svg { width: 20px; height: 20px; }
111
+ [role='tablist'] { gap: 8px; }
112
+ [role='tab'] { min-height: 48px; padding: 12px 14px; }
113
+ .kinds { gap: 8px; }
114
+ .kinds span { padding: 14px 18px; }
115
+ .details > label { min-height: 48px; gap: 12px; }
116
+ .details > label input { width: 24px; height: 24px; }
117
+ .primary { min-height: 48px; padding: 12px 24px; }
118
+ .secondary { min-height: 48px; padding: 12px 18px; }
119
+ summary { min-height: 48px; padding: 14px 12px; }
120
+ }
121
+ `;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineDialog = defineDialog;
4
+ const css_js_1 = require("./css.js");
5
+ const dom_js_1 = require("./dom.js");
6
+ const lists_js_1 = require("./lists.js");
7
+ const submit_js_1 = require("./submit.js");
8
+ const TABS = [
9
+ ['submit', 'Submit'],
10
+ ['active', 'Active'],
11
+ ['qa', 'Q&A'],
12
+ ];
13
+ function closeIcon() {
14
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
15
+ svg.setAttribute('viewBox', '0 0 16 16');
16
+ svg.setAttribute('aria-hidden', 'true');
17
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
18
+ path.setAttribute('d', 'M3 3l10 10M13 3L3 13');
19
+ svg.append(path);
20
+ return svg;
21
+ }
22
+ function defineDialog(getHost) {
23
+ return class CrawlSpaceDialog extends HTMLElement {
24
+ root;
25
+ dialog;
26
+ heading = (0, dom_js_1.h)('h2', { id: 'cs-title' });
27
+ tabs = new Map();
28
+ panels = new Map();
29
+ current = 'submit';
30
+ constructor() {
31
+ super();
32
+ const host = getHost();
33
+ this.root = this.attachShadow({ mode: 'open' });
34
+ const close = (0, dom_js_1.h)('button', { type: 'button', class: 'close', 'aria-label': 'Close' }, closeIcon());
35
+ close.addEventListener('click', () => this.hide());
36
+ const tablist = (0, dom_js_1.h)('div', { role: 'tablist', 'aria-labelledby': 'cs-title' });
37
+ tablist.addEventListener('keydown', (event) => this.onTabKey(event));
38
+ this.dialog = (0, dom_js_1.h)('dialog', { 'aria-labelledby': 'cs-title' }, (0, dom_js_1.h)('header', {}, this.heading, close), tablist);
39
+ const panels = { submit: (0, submit_js_1.createSubmitPanel)(host), active: (0, lists_js_1.createActivePanel)(host), qa: (0, lists_js_1.createQaPanel)(host) };
40
+ for (const [tab, label] of TABS) {
41
+ const button = (0, dom_js_1.h)('button', { type: 'button', role: 'tab', id: `cs-tab-${tab}`, 'aria-controls': `cs-panel-${tab}` }, label);
42
+ button.addEventListener('click', () => this.select(tab));
43
+ tablist.append(button);
44
+ this.tabs.set(tab, button);
45
+ const element = (0, dom_js_1.h)('div', { role: 'tabpanel', id: `cs-panel-${tab}`, 'aria-labelledby': `cs-tab-${tab}`, tabindex: '0' }, panels[tab].element);
46
+ this.dialog.append(element);
47
+ this.panels.set(tab, { panel: panels[tab], element });
48
+ }
49
+ this.dialog.addEventListener('close', () => getHost().options.onOpenChange?.(false));
50
+ this.root.append(this.dialog);
51
+ }
52
+ connectedCallback() {
53
+ getHost().connected.add(this);
54
+ this.refresh();
55
+ }
56
+ disconnectedCallback() {
57
+ getHost().connected.delete(this);
58
+ }
59
+ refresh() {
60
+ const host = getHost();
61
+ host.styles.apply(this.root, css_js_1.DIALOG_CSS);
62
+ this.heading.textContent = host.options.title;
63
+ for (const { panel } of this.panels.values())
64
+ panel.refresh?.();
65
+ }
66
+ showTab(tab) {
67
+ this.select(tab);
68
+ if (this.dialog.open)
69
+ return;
70
+ this.dialog.showModal();
71
+ this.tabs.get(tab)?.focus();
72
+ getHost().options.onOpenChange?.(true);
73
+ }
74
+ hide() {
75
+ if (this.dialog.open)
76
+ this.dialog.close();
77
+ }
78
+ select(tab) {
79
+ this.current = tab;
80
+ for (const [name, button] of this.tabs) {
81
+ const selected = name === tab;
82
+ button.setAttribute('aria-selected', String(selected));
83
+ button.tabIndex = selected ? 0 : -1;
84
+ }
85
+ for (const [name, { element }] of this.panels)
86
+ element.hidden = name !== tab;
87
+ this.panels.get(tab)?.panel.show();
88
+ }
89
+ onTabKey(event) {
90
+ const order = TABS.map(([tab]) => tab);
91
+ const index = order.indexOf(this.current);
92
+ const moves = {
93
+ ArrowRight: (index + 1) % order.length,
94
+ ArrowLeft: (index + order.length - 1) % order.length,
95
+ Home: 0,
96
+ End: order.length - 1,
97
+ };
98
+ const next = moves[event.key];
99
+ if (next === undefined)
100
+ return;
101
+ event.preventDefault();
102
+ this.select(order[next]);
103
+ this.tabs.get(order[next])?.focus();
104
+ }
105
+ };
106
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.h = h;
4
+ function h(tag, attributes = {}, ...children) {
5
+ const element = document.createElement(tag);
6
+ for (const [name, value] of Object.entries(attributes)) {
7
+ if (value === true)
8
+ element.setAttribute(name, '');
9
+ else if (value !== false)
10
+ element.setAttribute(name, value);
11
+ }
12
+ for (const child of children) {
13
+ if (child !== null && child !== undefined && child !== false)
14
+ element.append(child);
15
+ }
16
+ return element;
17
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeText = exports.createCrawlSpace = void 0;
4
+ var crawl_space_js_1 = require("./crawl-space.js");
5
+ Object.defineProperty(exports, "createCrawlSpace", { enumerable: true, get: function () { return crawl_space_js_1.createCrawlSpace; } });
6
+ var sanitize_js_1 = require("./sanitize.js");
7
+ Object.defineProperty(exports, "sanitizeText", { enumerable: true, get: function () { return sanitize_js_1.sanitizeText; } });
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createActivePanel = createActivePanel;
4
+ exports.createQaPanel = createQaPanel;
5
+ const dom_js_1 = require("./dom.js");
6
+ const TYPE_LABELS = { bug: 'Bug', question: 'Question' };
7
+ const STATUS_LABELS = { new: 'New', in_progress: 'In progress', resolved: 'Resolved', dismissed: 'Dismissed' };
8
+ function note(text) {
9
+ return (0, dom_js_1.h)('p', { class: 'note' }, text);
10
+ }
11
+ function dateOf(iso) {
12
+ const label = new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
13
+ return (0, dom_js_1.h)('time', { datetime: iso }, label);
14
+ }
15
+ function reportItem(report, own) {
16
+ return (0, dom_js_1.h)('li', { class: 'item' }, (0, dom_js_1.h)('div', { class: 'meta' }, (0, dom_js_1.h)('span', { class: 'badge' }, TYPE_LABELS[report.type]), own && (0, dom_js_1.h)('span', { class: `badge ${report.status}` }, STATUS_LABELS[report.status]), dateOf(report.createdAt)), (0, dom_js_1.h)('p', { class: 'text' }, report.description), own &&
17
+ report.answer !== null &&
18
+ (0, dom_js_1.h)('div', { class: 'answer' }, (0, dom_js_1.h)('div', { class: 'answer-label' }, 'Answer'), (0, dom_js_1.h)('p', { class: 'text' }, report.answer)));
19
+ }
20
+ function section(title, content) {
21
+ return (0, dom_js_1.h)('section', {}, (0, dom_js_1.h)('h3', {}, title), content);
22
+ }
23
+ function renderReports(reports) {
24
+ const sections = [];
25
+ if (reports.mine.length > 0) {
26
+ sections.push(section('Your reports', (0, dom_js_1.h)('ul', { class: 'list' }, ...reports.mine.map((r) => reportItem(r, true)))));
27
+ }
28
+ sections.push(section('Being worked on', reports.active.length > 0
29
+ ? (0, dom_js_1.h)('ul', { class: 'list' }, ...reports.active.map((r) => reportItem(r, false)))
30
+ : note('Nothing is being worked on right now.')));
31
+ return sections;
32
+ }
33
+ function renderQa(items) {
34
+ if (items.length === 0)
35
+ return [note('No questions have been answered yet.')];
36
+ const entries = items.map((item) => (0, dom_js_1.h)('li', {}, (0, dom_js_1.h)('details', {}, (0, dom_js_1.h)('summary', {}, item.question), (0, dom_js_1.h)('p', { class: 'text' }, item.answer))));
37
+ return [(0, dom_js_1.h)('ul', { class: 'list' }, ...entries)];
38
+ }
39
+ function listPanel(load, render) {
40
+ const element = (0, dom_js_1.h)('div');
41
+ let latest = 0;
42
+ function show() {
43
+ const request = ++latest;
44
+ element.setAttribute('aria-busy', 'true');
45
+ element.replaceChildren(note('Loading...'));
46
+ void load().then((result) => {
47
+ if (request !== latest)
48
+ return;
49
+ element.removeAttribute('aria-busy');
50
+ if (result.ok) {
51
+ element.replaceChildren(...render(result.value));
52
+ return;
53
+ }
54
+ const retry = (0, dom_js_1.h)('button', { type: 'button', class: 'secondary' }, 'Try again');
55
+ retry.addEventListener('click', show);
56
+ const text = result.reason === 'rate-limited' ? 'Too many requests. Please try again in a minute.' : 'This could not be loaded.';
57
+ element.replaceChildren((0, dom_js_1.h)('div', { class: 'failed' }, (0, dom_js_1.h)('p', { class: 'message error' }, text), retry));
58
+ });
59
+ }
60
+ return { element, show };
61
+ }
62
+ function createActivePanel(host) {
63
+ return listPanel(() => host.api.getReports(), renderReports);
64
+ }
65
+ function createQaPanel(host) {
66
+ return listPanel(() => host.api.getQa(), renderQa);
67
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toTab = toTab;
4
+ exports.resolveOptions = resolveOptions;
5
+ const themes_js_1 = require("./themes.js");
6
+ const POSITIONS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
7
+ const TABS = ['submit', 'active', 'qa'];
8
+ function warn(message) {
9
+ console.warn(`[crawl-space] ${message}`);
10
+ }
11
+ function isRecord(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ function text(name, value, fallback) {
15
+ if (value === undefined)
16
+ return fallback;
17
+ if (typeof value === 'string' && value.trim() !== '')
18
+ return value;
19
+ warn(`"${name}" must be a non-empty string; using "${fallback}".`);
20
+ return fallback;
21
+ }
22
+ function fn(name, value) {
23
+ if (value === undefined || typeof value === 'function')
24
+ return value;
25
+ warn(`"${name}" must be a function; ignoring it.`);
26
+ return undefined;
27
+ }
28
+ function floating(value) {
29
+ if (value === undefined)
30
+ return null;
31
+ if (!isRecord(value)) {
32
+ warn('"floating" must be an object; no floating button.');
33
+ return null;
34
+ }
35
+ const position = value.position ?? 'bottom-right';
36
+ if (POSITIONS.includes(position))
37
+ return position;
38
+ warn(`Unknown floating position "${String(position)}"; using "bottom-right".`);
39
+ return 'bottom-right';
40
+ }
41
+ function toTab(value) {
42
+ if (TABS.includes(value))
43
+ return value;
44
+ if (value !== undefined && value !== null)
45
+ warn(`Unknown tab "${String(value)}"; using "submit".`);
46
+ return 'submit';
47
+ }
48
+ function resolveOptions(options) {
49
+ if (!isRecord(options) || typeof options.api !== 'string' || options.api.trim() === '') {
50
+ throw new TypeError('[crawl-space] "api" must be a non-empty string.');
51
+ }
52
+ let paths = {};
53
+ if (isRecord(options.paths))
54
+ paths = options.paths;
55
+ else if (options.paths !== undefined)
56
+ warn('"paths" must be an object; using the default paths.');
57
+ let maxLength = 2000;
58
+ if (Number.isInteger(options.maxLength) && options.maxLength > 0)
59
+ maxLength = options.maxLength;
60
+ else if (options.maxLength !== undefined)
61
+ warn(`"maxLength" must be a positive whole number; using ${maxLength}.`);
62
+ let version;
63
+ if (typeof options.version === 'string')
64
+ version = options.version;
65
+ else if (options.version !== undefined)
66
+ warn('"version" must be a string; ignoring it.');
67
+ return {
68
+ api: options.api,
69
+ paths: { reports: text('paths.reports', paths.reports, '/reports'), qa: text('paths.qa', paths.qa, '/qa') },
70
+ colors: (0, themes_js_1.resolveColors)(options),
71
+ floating: floating(options.floating),
72
+ title: text('title', options.title, 'Help & bug reports'),
73
+ maxLength,
74
+ version,
75
+ getToken: fn('getToken', options.getToken),
76
+ scrubPath: fn('scrubPath', options.scrubPath),
77
+ getContext: fn('getContext', options.getContext),
78
+ onOpenChange: fn('onOpenChange', options.onOpenChange),
79
+ };
80
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeText = sanitizeText;
4
+ const INVISIBLE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u200b-\u200d\u2060\ufeff\u202a-\u202e\u2066-\u2069]/g;
5
+ const BLANK_RUN = /\n(?:[ \t]*\n){3,}/g;
6
+ function sanitizeText(text, maxLength) {
7
+ let clean = text
8
+ .normalize('NFC')
9
+ .replace(/\r\n/g, '\n')
10
+ .replace(INVISIBLE, '')
11
+ .replace(BLANK_RUN, '\n\n')
12
+ .trim();
13
+ if (clean.length > maxLength) {
14
+ let end = Math.max(0, maxLength);
15
+ const last = clean.charCodeAt(end - 1);
16
+ if (last >= 0xd800 && last <= 0xdbff)
17
+ end -= 1;
18
+ clean = clean.slice(0, end).trimEnd();
19
+ }
20
+ return clean;
21
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.themeCss = themeCss;
4
+ exports.createStyles = createStyles;
5
+ const PROPERTIES = {
6
+ background: '--cs-background',
7
+ surface: '--cs-surface',
8
+ text: '--cs-text',
9
+ muted: '--cs-muted',
10
+ border: '--cs-border',
11
+ accent: '--cs-accent',
12
+ accentText: '--cs-accent-text',
13
+ error: '--cs-error',
14
+ success: '--cs-success',
15
+ font: '--cs-font',
16
+ };
17
+ function themeCss(colors) {
18
+ const lines = Object.keys(PROPERTIES).map((key) => `${PROPERTIES[key]}: ${colors[key]};`);
19
+ return `:host { ${lines.join(' ')} }`;
20
+ }
21
+ function supportsConstructable() {
22
+ return (typeof CSSStyleSheet !== 'undefined' &&
23
+ 'replaceSync' in CSSStyleSheet.prototype &&
24
+ typeof ShadowRoot !== 'undefined' &&
25
+ 'adoptedStyleSheets' in ShadowRoot.prototype);
26
+ }
27
+ function sheetFor(css) {
28
+ const sheet = new CSSStyleSheet();
29
+ sheet.replaceSync(css);
30
+ return sheet;
31
+ }
32
+ function styleIn(root, name) {
33
+ let style = root.querySelector(`style[data-cs="${name}"]`);
34
+ if (!style) {
35
+ style = document.createElement('style');
36
+ style.dataset.cs = name;
37
+ root.prepend(style);
38
+ }
39
+ return style;
40
+ }
41
+ function createStyles(colors) {
42
+ if (supportsConstructable()) {
43
+ const theme = sheetFor(themeCss(colors));
44
+ const sheets = new Map();
45
+ return {
46
+ apply(root, css) {
47
+ let sheet = sheets.get(css);
48
+ if (!sheet) {
49
+ sheet = sheetFor(css);
50
+ sheets.set(css, sheet);
51
+ }
52
+ root.adoptedStyleSheets = [sheet, theme];
53
+ },
54
+ setColors(next) {
55
+ theme.replaceSync(themeCss(next));
56
+ },
57
+ };
58
+ }
59
+ let theme = themeCss(colors);
60
+ return {
61
+ apply(root, css) {
62
+ styleIn(root, 'theme').textContent = theme;
63
+ styleIn(root, 'base').textContent = css;
64
+ },
65
+ setColors(next) {
66
+ theme = themeCss(next);
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSubmitPanel = createSubmitPanel;
4
+ const context_js_1 = require("./context.js");
5
+ const dom_js_1 = require("./dom.js");
6
+ const sanitize_js_1 = require("./sanitize.js");
7
+ const PROMPTS = { bug: 'What went wrong?', question: 'What would you like to know?' };
8
+ function createSubmitPanel(host) {
9
+ const bug = (0, dom_js_1.h)('input', { type: 'radio', name: 'cs-type', value: 'bug', checked: true });
10
+ const question = (0, dom_js_1.h)('input', { type: 'radio', name: 'cs-type', value: 'question' });
11
+ const prompt = (0, dom_js_1.h)('label', { class: 'field-label', for: 'cs-description' });
12
+ const description = (0, dom_js_1.h)('textarea', { id: 'cs-description', rows: '6' });
13
+ const counter = (0, dom_js_1.h)('div', { class: 'counter' });
14
+ const include = (0, dom_js_1.h)('input', { type: 'checkbox', checked: true });
15
+ const details = (0, dom_js_1.h)('ul', { 'aria-label': 'Details sent with it' });
16
+ const detailsBox = (0, dom_js_1.h)('div', { class: 'details' }, (0, dom_js_1.h)('label', {}, include, 'Include these details'), details);
17
+ const message = (0, dom_js_1.h)('p', { class: 'message', role: 'status' });
18
+ const send = (0, dom_js_1.h)('button', { type: 'submit', class: 'primary' }, 'Send');
19
+ const form = (0, dom_js_1.h)('form', {}, (0, dom_js_1.h)('fieldset', { class: 'kinds' }, (0, dom_js_1.h)('legend', { class: 'hidden-label' }, 'Type'), (0, dom_js_1.h)('label', {}, bug, (0, dom_js_1.h)('span', {}, 'Bug')), (0, dom_js_1.h)('label', {}, question, (0, dom_js_1.h)('span', {}, 'Question'))), prompt, description, counter, detailsBox, (0, dom_js_1.h)('div', { class: 'actions' }, message, send));
20
+ let shown = [];
21
+ function say(kind, text) {
22
+ message.className = `message ${kind}`.trim();
23
+ message.textContent = text;
24
+ }
25
+ function update() {
26
+ prompt.textContent = question.checked ? PROMPTS.question : PROMPTS.bug;
27
+ counter.textContent = `${description.value.length} / ${host.options.maxLength}`;
28
+ details.classList.toggle('off', !include.checked);
29
+ for (const { field } of shown)
30
+ if (field)
31
+ field.disabled = !include.checked;
32
+ }
33
+ function contextToSend() {
34
+ const pairs = shown.flatMap(({ entry, field }) => {
35
+ const value = field ? (0, sanitize_js_1.sanitizeText)(field.value, context_js_1.CONTEXT_LIMITS.maxValueLength) : entry.value;
36
+ return value === '' ? [] : [[entry.label, value]];
37
+ });
38
+ return pairs.length > 0 ? Object.fromEntries(pairs) : undefined;
39
+ }
40
+ form.addEventListener('change', update);
41
+ description.addEventListener('input', () => {
42
+ say('', '');
43
+ update();
44
+ });
45
+ form.addEventListener('submit', (event) => {
46
+ event.preventDefault();
47
+ const text = (0, sanitize_js_1.sanitizeText)(description.value, host.options.maxLength);
48
+ if (text === '') {
49
+ say('error', 'Write something first.');
50
+ description.focus();
51
+ return;
52
+ }
53
+ const type = question.checked ? 'question' : 'bug';
54
+ const context = include.checked ? contextToSend() : undefined;
55
+ send.disabled = true;
56
+ say('', 'Sending...');
57
+ void host.api.submitReport({ type, description: text, ...(context && { context }) }).then((result) => {
58
+ send.disabled = false;
59
+ if (result.ok) {
60
+ form.reset();
61
+ update();
62
+ say('success', `Thanks! Your ${type === 'bug' ? 'report' : 'question'} was sent.`);
63
+ }
64
+ else if (result.reason === 'rate-limited') {
65
+ say('error', 'Too many sent in a short time. Please try again in a minute.');
66
+ }
67
+ else {
68
+ say('error', 'It could not be sent. Please try again.');
69
+ }
70
+ });
71
+ });
72
+ return {
73
+ element: form,
74
+ show() {
75
+ const before = new Map(shown.map((detail) => [detail.entry.label, detail]));
76
+ shown = (0, context_js_1.collectContext)(host.options).map((entry) => {
77
+ if (!entry.editable)
78
+ return { entry };
79
+ const field = (0, dom_js_1.h)('input', { type: 'text', value: entry.value, maxlength: String(context_js_1.CONTEXT_LIMITS.maxValueLength) });
80
+ const previous = before.get(entry.label);
81
+ if (previous?.field && previous.entry.value === entry.value)
82
+ field.value = previous.field.value;
83
+ return { entry, field };
84
+ });
85
+ details.replaceChildren(...shown.map(({ entry, field }) => field
86
+ ? (0, dom_js_1.h)('li', { class: 'editable' }, (0, dom_js_1.h)('label', {}, (0, dom_js_1.h)('span', { class: 'label' }, `${entry.label}: `), field))
87
+ : (0, dom_js_1.h)('li', {}, (0, dom_js_1.h)('span', { class: 'label' }, `${entry.label}: `), entry.value)));
88
+ detailsBox.hidden = shown.length === 0;
89
+ update();
90
+ },
91
+ refresh() {
92
+ description.maxLength = host.options.maxLength;
93
+ update();
94
+ },
95
+ };
96
+ }