@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.
- package/LICENSE +21 -0
- package/README.md +248 -2
- package/dist/cjs/api.js +90 -0
- package/dist/cjs/button.js +33 -0
- package/dist/cjs/context.js +72 -0
- package/dist/cjs/crawl-space.js +76 -0
- package/dist/cjs/css.js +104 -0
- package/dist/cjs/dialog.js +106 -0
- package/dist/cjs/dom.js +17 -0
- package/dist/cjs/host.js +2 -0
- package/dist/cjs/index.js +7 -0
- package/dist/cjs/lists.js +67 -0
- package/dist/cjs/options.js +80 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/sanitize.js +21 -0
- package/dist/cjs/styles.js +69 -0
- package/dist/cjs/submit.js +96 -0
- package/dist/cjs/themes.js +89 -0
- package/dist/cjs/types.js +2 -0
- package/dist/esm/api.js +86 -0
- package/dist/esm/button.js +30 -0
- package/dist/esm/context.js +68 -0
- package/dist/esm/crawl-space.js +73 -0
- package/dist/esm/css.js +101 -0
- package/dist/esm/dialog.js +103 -0
- package/dist/esm/dom.js +14 -0
- package/dist/esm/host.js +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/lists.js +63 -0
- package/dist/esm/options.js +76 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/sanitize.js +18 -0
- package/dist/esm/styles.js +65 -0
- package/dist/esm/submit.js +93 -0
- package/dist/esm/themes.js +85 -0
- package/dist/esm/types.js +1 -0
- package/dist/types/api.d.ts +20 -0
- package/dist/types/button.d.ts +8 -0
- package/dist/types/context.d.ts +15 -0
- package/dist/types/crawl-space.d.ts +11 -0
- package/dist/types/css.d.ts +2 -0
- package/dist/types/dialog.d.ts +13 -0
- package/dist/types/dom.d.ts +6 -0
- package/dist/types/host.d.ts +17 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/lists.d.ts +9 -0
- package/dist/types/options.d.ts +23 -0
- package/dist/types/sanitize.d.ts +7 -0
- package/dist/types/styles.d.ts +14 -0
- package/dist/types/submit.d.ts +4 -0
- package/dist/types/themes.d.ts +5 -0
- package/dist/types/types.d.ts +112 -0
- package/package.json +60 -4
|
@@ -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
|
+
}
|
package/dist/cjs/dom.js
ADDED
|
@@ -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
|
+
}
|
package/dist/cjs/host.js
ADDED
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.THEMES = void 0;
|
|
4
|
+
exports.resolveColors = resolveColors;
|
|
5
|
+
const DEFAULT = {
|
|
6
|
+
background: '#121417',
|
|
7
|
+
surface: '#1c1f24',
|
|
8
|
+
text: '#e8eaed',
|
|
9
|
+
muted: '#9aa0a6',
|
|
10
|
+
border: '#33373d',
|
|
11
|
+
accent: '#5b9cff',
|
|
12
|
+
accentText: '#0b1220',
|
|
13
|
+
error: '#ff6b6b',
|
|
14
|
+
success: '#6fcf97',
|
|
15
|
+
font: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
|
|
16
|
+
};
|
|
17
|
+
const BROCODE = {
|
|
18
|
+
background: '#181818',
|
|
19
|
+
surface: '#222222',
|
|
20
|
+
text: '#ffffff',
|
|
21
|
+
muted: '#b0b0b0',
|
|
22
|
+
border: '#3c3c3c',
|
|
23
|
+
accent: '#7b9981',
|
|
24
|
+
accentText: '#ffffff',
|
|
25
|
+
error: '#ff5252',
|
|
26
|
+
success: '#96be4d',
|
|
27
|
+
font: 'Roboto, sans-serif',
|
|
28
|
+
};
|
|
29
|
+
const ROMBIE = {
|
|
30
|
+
background: '#0a0a12',
|
|
31
|
+
surface: '#15141f',
|
|
32
|
+
text: '#f1eef9',
|
|
33
|
+
muted: '#9a90bd',
|
|
34
|
+
border: '#302c47',
|
|
35
|
+
accent: '#ff2d78',
|
|
36
|
+
accentText: '#12060d',
|
|
37
|
+
error: '#ff5c5c',
|
|
38
|
+
success: '#00e5ff',
|
|
39
|
+
font: "system-ui, 'Segoe UI', Roboto, sans-serif",
|
|
40
|
+
};
|
|
41
|
+
exports.THEMES = {
|
|
42
|
+
default: DEFAULT,
|
|
43
|
+
brocode: BROCODE,
|
|
44
|
+
rombie: ROMBIE,
|
|
45
|
+
'taut-wad': BROCODE,
|
|
46
|
+
custom: DEFAULT,
|
|
47
|
+
};
|
|
48
|
+
const COLOR_KEYS = Object.keys(DEFAULT);
|
|
49
|
+
function isTheme(name) {
|
|
50
|
+
return Object.hasOwn(exports.THEMES, name);
|
|
51
|
+
}
|
|
52
|
+
function isValid(key, value) {
|
|
53
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
54
|
+
return false;
|
|
55
|
+
return CSS.supports(key === 'font' ? 'font-family' : 'color', value);
|
|
56
|
+
}
|
|
57
|
+
function resolveColors(config) {
|
|
58
|
+
const theme = config.theme ?? 'default';
|
|
59
|
+
if (typeof theme !== 'string' || !isTheme(theme)) {
|
|
60
|
+
console.warn(`[crawl-space] Unknown theme "${String(theme)}"; using "default".`);
|
|
61
|
+
return { ...DEFAULT };
|
|
62
|
+
}
|
|
63
|
+
if (theme !== 'custom') {
|
|
64
|
+
if (config.colors !== undefined) {
|
|
65
|
+
console.warn(`[crawl-space] "colors" is only used with theme "custom"; ignoring it.`);
|
|
66
|
+
}
|
|
67
|
+
return { ...exports.THEMES[theme] };
|
|
68
|
+
}
|
|
69
|
+
const colors = { ...DEFAULT };
|
|
70
|
+
const given = config.colors ?? {};
|
|
71
|
+
if (typeof given !== 'object' || given === null || Array.isArray(given)) {
|
|
72
|
+
console.warn('[crawl-space] "colors" must be an object; using the default theme.');
|
|
73
|
+
return colors;
|
|
74
|
+
}
|
|
75
|
+
for (const [key, value] of Object.entries(given)) {
|
|
76
|
+
if (value === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
if (!COLOR_KEYS.includes(key)) {
|
|
79
|
+
console.warn(`[crawl-space] Unknown color "${key}"; ignoring it.`);
|
|
80
|
+
}
|
|
81
|
+
else if (isValid(key, value)) {
|
|
82
|
+
colors[key] = value;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.warn(`[crawl-space] Invalid ${key} "${String(value)}"; using the default theme's.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return colors;
|
|
89
|
+
}
|
package/dist/esm/api.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const TYPES = ['bug', 'question'];
|
|
2
|
+
const STATUSES = ['new', 'in_progress', 'resolved', 'dismissed'];
|
|
3
|
+
const FAILED = { ok: false, reason: 'failed' };
|
|
4
|
+
export function joinUrl(base, path) {
|
|
5
|
+
return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
|
|
6
|
+
}
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function isId(value) {
|
|
11
|
+
return (typeof value === 'number' && Number.isFinite(value)) || (typeof value === 'string' && value !== '');
|
|
12
|
+
}
|
|
13
|
+
function isDate(value) {
|
|
14
|
+
return typeof value === 'string' && !Number.isNaN(Date.parse(value));
|
|
15
|
+
}
|
|
16
|
+
function isOneOf(list, value) {
|
|
17
|
+
return list.includes(value);
|
|
18
|
+
}
|
|
19
|
+
function toReport(item) {
|
|
20
|
+
if (!isRecord(item))
|
|
21
|
+
return null;
|
|
22
|
+
const { id, type, status, description, answer, createdAt, updatedAt } = item;
|
|
23
|
+
if (!isId(id) || !isOneOf(TYPES, type) || !isOneOf(STATUSES, status) || typeof description !== 'string')
|
|
24
|
+
return null;
|
|
25
|
+
if (answer != null && typeof answer !== 'string')
|
|
26
|
+
return null;
|
|
27
|
+
if (!isDate(createdAt) || !isDate(updatedAt))
|
|
28
|
+
return null;
|
|
29
|
+
return { id, type, status, description, answer: answer ?? null, createdAt, updatedAt };
|
|
30
|
+
}
|
|
31
|
+
function toQaItem(item) {
|
|
32
|
+
if (!isRecord(item))
|
|
33
|
+
return null;
|
|
34
|
+
const { id, question, answer, updatedAt } = item;
|
|
35
|
+
if (!isId(id) || typeof question !== 'string' || typeof answer !== 'string' || !isDate(updatedAt))
|
|
36
|
+
return null;
|
|
37
|
+
return { id, question, answer, updatedAt };
|
|
38
|
+
}
|
|
39
|
+
function listOf(value, toItem) {
|
|
40
|
+
if (!Array.isArray(value))
|
|
41
|
+
return null;
|
|
42
|
+
return value.map(toItem).filter((item) => item !== null);
|
|
43
|
+
}
|
|
44
|
+
function toReports(body) {
|
|
45
|
+
if (!isRecord(body))
|
|
46
|
+
return null;
|
|
47
|
+
const mine = listOf(body.mine, toReport);
|
|
48
|
+
const active = listOf(body.active, toReport);
|
|
49
|
+
return mine && active ? { mine, active } : null;
|
|
50
|
+
}
|
|
51
|
+
function toCreated(body) {
|
|
52
|
+
return isRecord(body) && isId(body.id) ? { id: body.id } : null;
|
|
53
|
+
}
|
|
54
|
+
export function createApiClient(options) {
|
|
55
|
+
const reportsUrl = joinUrl(options.api, options.paths?.reports ?? '/reports');
|
|
56
|
+
const qaUrl = joinUrl(options.api, options.paths?.qa ?? '/qa');
|
|
57
|
+
async function request(url, parse, body) {
|
|
58
|
+
try {
|
|
59
|
+
const headers = { Accept: 'application/json' };
|
|
60
|
+
if (body !== undefined)
|
|
61
|
+
headers['Content-Type'] = 'application/json';
|
|
62
|
+
const token = await options.getToken?.();
|
|
63
|
+
if (typeof token === 'string' && token !== '')
|
|
64
|
+
headers.Authorization = `Bearer ${token}`;
|
|
65
|
+
const response = await fetch(url, {
|
|
66
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
67
|
+
headers,
|
|
68
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
69
|
+
});
|
|
70
|
+
if (response.status === 429)
|
|
71
|
+
return { ok: false, reason: 'rate-limited' };
|
|
72
|
+
if (!response.ok)
|
|
73
|
+
return FAILED;
|
|
74
|
+
const value = parse(await response.json());
|
|
75
|
+
return value === null ? FAILED : { ok: true, value };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return FAILED;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
submitReport: (submission) => request(reportsUrl, toCreated, submission),
|
|
83
|
+
getReports: () => request(reportsUrl, toReports),
|
|
84
|
+
getQa: () => request(qaUrl, (body) => listOf(body, toQaItem)),
|
|
85
|
+
};
|
|
86
|
+
}
|