@bloodhound-rs/playground 0.0.0-stage → 0.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 +139 -2
- package/dist/ManifestPlayground.d.ts +9 -0
- package/dist/ManifestPlayground.js +12 -0
- package/dist/ManifestSelector.d.ts +23 -0
- package/dist/ManifestSelector.js +51 -0
- package/dist/ManifestViewer.d.ts +17 -0
- package/dist/ManifestViewer.js +90 -0
- package/dist/SourcePreview.d.ts +11 -0
- package/dist/SourcePreview.js +81 -0
- package/dist/components.d.ts +56 -0
- package/dist/components.js +29 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/manifest.d.ts +1 -0
- package/dist/manifest.js +1 -0
- package/dist/styles.css +117 -0
- package/package.json +47 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Justinidlerz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,140 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bloodhound-rs/playground
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React 18/19 components for Manifest v2. The viewer and resource selector are independent exports. Neither depends on the scanner's native binary or an application router.
|
|
4
|
+
|
|
5
|
+
| Export | Responsibility |
|
|
6
|
+
| -------------------- | ------------------------------------------------------------------------------------------ |
|
|
7
|
+
| `ManifestViewer` | Render an already loaded manifest, filter records, inspect dependencies and preview source |
|
|
8
|
+
| `ManifestSelector` | Read a local JSON file or manifest URL and call `onChange` |
|
|
9
|
+
| `ManifestPlayground` | Ready-to-use selector and viewer composition |
|
|
10
|
+
| `ManifestComponents` | UI component slots, including an optional replacement for the source viewer |
|
|
11
|
+
|
|
12
|
+
## Install and use
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add @bloodhound-rs/playground
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
React 18 or 19 is supplied by the host application. JavaScript, TypeScript declarations and CSS are included in the package.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { ManifestPlayground } from '@bloodhound-rs/playground';
|
|
22
|
+
import '@bloodhound-rs/playground/styles.css';
|
|
23
|
+
|
|
24
|
+
<ManifestPlayground components={components} loadText={loadText} />;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`initialResource` preloads a parsed report when mounting. `onChange(resource)` receives resource selections. `selectorProps` forwards `loadManifest`, `parse` and `maxBytes` to the selector. The remaining props go to `ManifestViewer`. Hosts that already manage the selected build use `ManifestViewer` directly.
|
|
28
|
+
|
|
29
|
+
Headless parsing is available without importing React components:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { parseManifest, ManifestIndex } from '@bloodhound-rs/playground/manifest';
|
|
33
|
+
|
|
34
|
+
const { manifest } = parseManifest(jsonText);
|
|
35
|
+
const index = new ManifestIndex(manifest);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Ant Design example
|
|
39
|
+
|
|
40
|
+
Install `antd` in the host application and pass its components with adapters for input events and table data. The host loads the manifest; this example renders the report without a resource selector.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { Button, Input, Select, Table, Tag } from 'antd';
|
|
44
|
+
import { ManifestViewer, type Manifest, type ManifestComponents } from '@bloodhound-rs/playground';
|
|
45
|
+
import '@bloodhound-rs/playground/styles.css';
|
|
46
|
+
|
|
47
|
+
const components: Partial<ManifestComponents> = {
|
|
48
|
+
Button: (props) => <Button {...props} />,
|
|
49
|
+
Select,
|
|
50
|
+
Tag,
|
|
51
|
+
Input: ({ onChange, ...props }) => (
|
|
52
|
+
<Input {...props} onChange={(event) => onChange(event.target.value)} />
|
|
53
|
+
),
|
|
54
|
+
Table: ({ data, ...props }) => <Table {...props} dataSource={data} />,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function ScanReport({ manifest }: { manifest: Manifest }) {
|
|
58
|
+
return <ManifestViewer manifest={manifest} components={components} />;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Ant Design's `Input.onChange` receives an event, while the viewer expects a string. Its `Table` uses `dataSource`, while the viewer supplies `data`. The adapters above translate these props. Define the component map outside rendering to keep component identities stable.
|
|
63
|
+
|
|
64
|
+
Bloodhound does not depend on a UI library or inject a theme provider. Its CSS only styles the viewer's layout, default table and code excerpt, under `bh-` classes. Existing providers and component themes remain owned by the host.
|
|
65
|
+
|
|
66
|
+
Components with different props use a small adapter. `Input.onChange` and `Select.onChange` receive a string. `Table` receives `data`, `columns`, `rowKey="id"` and `pagination={false}`. Missing slots use native HTML controls.
|
|
67
|
+
|
|
68
|
+
## Source loading
|
|
69
|
+
|
|
70
|
+
`manifestUrl` resolves relative map URLs. `loadText` routes generated code, source maps and original files through the host's resource service. The callback returns text and its absolute resource URL after redirects. No fetched code is executed.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { ManifestViewer, type TextLoader } from '@bloodhound-rs/playground';
|
|
74
|
+
|
|
75
|
+
// Defined outside rendering; bind the selected build's ID in the host's service layer.
|
|
76
|
+
const loadText: TextLoader = async (url, request) => {
|
|
77
|
+
return resourceService.readText({
|
|
78
|
+
url,
|
|
79
|
+
file: request.file,
|
|
80
|
+
kind: request.kind,
|
|
81
|
+
maxBytes: request.limit,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
<ManifestViewer
|
|
86
|
+
manifest={manifest}
|
|
87
|
+
manifestUrl={manifestUrl}
|
|
88
|
+
components={components}
|
|
89
|
+
loadText={loadText}
|
|
90
|
+
/>;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`resourceService` above is the host's implementation. `request.file` carries the project-relative path recorded by the plugin, when available. `request.kind` is `source` or `sourcemap`. A copied build artifact can therefore be resolved by build ID and path even when the public URL has changed. The custom loader owns authentication and enforces `request.limit`.
|
|
94
|
+
|
|
95
|
+
With no loader, public HTTP(S) resources are fetched without credentials. The local Playground passes `workspace={{ projectRoot, fileEndpoint }}` to read files through its project endpoint. Original source is loaded only after opening a preview: project/host file first, then `sourcesContent`, then a resolvable source URL. A missing map association hides the original-source preview; users can explicitly link a map URL or, with a workspace/custom loader, a project path. Links last for the current report session.
|
|
96
|
+
|
|
97
|
+
To use the platform's Monaco wrapper, supply `components.SourceCode`. Its props are `{ text, path?, location? }`; `location` uses 1-based lines and 0-based UTF-16 columns. Convert columns to Monaco's 1-based convention. The slot controls readonly rendering, highlighting and scrolling.
|
|
98
|
+
|
|
99
|
+
## Compose the selector and viewer
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { useState } from 'react';
|
|
103
|
+
import { ManifestSelector, ManifestViewer, type ManifestResource } from '@bloodhound-rs/playground';
|
|
104
|
+
import '@bloodhound-rs/playground/styles.css';
|
|
105
|
+
|
|
106
|
+
export function Playground() {
|
|
107
|
+
const [resource, setResource] = useState<ManifestResource>();
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
<ManifestSelector onChange={setResource} />
|
|
111
|
+
{resource ? (
|
|
112
|
+
<ManifestViewer
|
|
113
|
+
manifest={resource.manifest}
|
|
114
|
+
manifestUrl={resource.url}
|
|
115
|
+
notices={resource.notices}
|
|
116
|
+
/>
|
|
117
|
+
) : null}
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`ManifestSelector.loadManifest(url)` replaces URL fetching. `parse(text)` accepts a synchronous or asynchronous parser, including a host-managed worker. `maxBytes` defaults to 64 MiB. Local files remain in the browser; URL resources follow the browser's CORS rules unless the host supplies a loader.
|
|
124
|
+
|
|
125
|
+
`ManifestViewer` additionally accepts `pageSize` (default 50), `className`, and `onSelect(row)`. Its four record categories are findings, sources, dependencies and gaps. Keep `manifest`, `workspace` and `loadText` references stable while displaying the same build. Replacing any resource context resets selection, filters, map links and pending preview state. Independent viewer instances do not share state.
|
|
126
|
+
|
|
127
|
+
## Local development
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
pnpm build:playground-package
|
|
131
|
+
pnpm playground --manifest /path/to/bloodhound.manifest.json --project /path/to/project
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Open the printed **React components** URL (`/react.html`). It composes these exact exports. The classic Playground uses the same `@bloodhound-rs/manifest` parser and source resolver.
|
|
135
|
+
|
|
136
|
+
## Changesets and release artifacts
|
|
137
|
+
|
|
138
|
+
This public workspace package participates in the repository's existing Changesets release flow. Changesets versions `@bloodhound-rs/playground` and its shared manifest dependency. The release workflow builds workspace packages, packs their `dist` output and publishes dependencies before consumers. PR releases use the workflow's beta suffix and tag; main-branch releases use the stable version and latest tag.
|
|
139
|
+
|
|
140
|
+
`pnpm build:playground-package` builds the component package. `pnpm build:playground` produces the repository's standalone preview site. The npm package exports components, styles and parsing utilities; it does not start a server or mount itself into the host page.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ManifestResource, type ManifestSelectorProps } from './ManifestSelector.js';
|
|
2
|
+
import { type ManifestViewerProps } from './ManifestViewer.js';
|
|
3
|
+
export interface ManifestPlaygroundProps extends Omit<ManifestViewerProps, 'manifest' | 'manifestUrl' | 'notices'> {
|
|
4
|
+
initialResource?: ManifestResource;
|
|
5
|
+
onChange?: (resource: ManifestResource) => void;
|
|
6
|
+
selectorProps?: Pick<ManifestSelectorProps, 'loadManifest' | 'parse' | 'maxBytes'>;
|
|
7
|
+
}
|
|
8
|
+
/** Ready-to-use composition. Hosts with their own resource selection can use ManifestViewer alone. */
|
|
9
|
+
export declare function ManifestPlayground({ initialResource, onChange, selectorProps, ...viewerProps }: ManifestPlaygroundProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { ManifestSelector, } from './ManifestSelector.js';
|
|
4
|
+
import { ManifestViewer } from './ManifestViewer.js';
|
|
5
|
+
/** Ready-to-use composition. Hosts with their own resource selection can use ManifestViewer alone. */
|
|
6
|
+
export function ManifestPlayground({ initialResource, onChange, selectorProps, ...viewerProps }) {
|
|
7
|
+
const [resource, setResource] = useState(initialResource);
|
|
8
|
+
return (_jsxs(_Fragment, { children: [_jsx(ManifestSelector, { ...selectorProps, components: viewerProps.components, onChange: (resource) => {
|
|
9
|
+
setResource(resource);
|
|
10
|
+
onChange?.(resource);
|
|
11
|
+
} }), resource ? (_jsx(ManifestViewer, { ...viewerProps, manifest: resource.manifest, manifestUrl: resource.url, notices: resource.notices })) : null] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parseManifest, type Manifest } from '@bloodhound-rs/manifest';
|
|
2
|
+
import { type ManifestComponents } from './components.js';
|
|
3
|
+
export interface ManifestResource {
|
|
4
|
+
manifest: Manifest;
|
|
5
|
+
name: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
notices: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ManifestSelectorProps {
|
|
10
|
+
onChange: (resource: ManifestResource) => void;
|
|
11
|
+
components?: Partial<ManifestComponents>;
|
|
12
|
+
/** Host-controlled URL fetch, including authentication when needed. */
|
|
13
|
+
loadManifest?: (url: string) => Promise<{
|
|
14
|
+
text: string;
|
|
15
|
+
url: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** Supply a worker-backed parser for large reports. */
|
|
18
|
+
parse?: (text: string) => ReturnType<typeof parseManifest> | Promise<ReturnType<typeof parseManifest>>;
|
|
19
|
+
maxBytes?: number;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Resource input only. It does not render a report or load any source code. */
|
|
23
|
+
export declare function ManifestSelector({ onChange, components, loadManifest, parse, maxBytes, className, }: ManifestSelectorProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { fetchText, parseManifest, safeUrl } from '@bloodhound-rs/manifest';
|
|
4
|
+
import { defaultComponents } from './components.js';
|
|
5
|
+
const defaultLoadManifest = (url) => fetchText(url, 64 * 1024 * 1024);
|
|
6
|
+
/** Resource input only. It does not render a report or load any source code. */
|
|
7
|
+
export function ManifestSelector({ onChange, components, loadManifest = defaultLoadManifest, parse = parseManifest, maxBytes = 64 * 1024 * 1024, className = '', }) {
|
|
8
|
+
const { Button, Input } = { ...defaultComponents, ...components };
|
|
9
|
+
const [url, setUrl] = useState('');
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
const [error, setError] = useState('');
|
|
12
|
+
const fileInput = useRef(null);
|
|
13
|
+
const request = useRef(0);
|
|
14
|
+
useEffect(() => () => {
|
|
15
|
+
request.current++;
|
|
16
|
+
}, []);
|
|
17
|
+
async function open(file) {
|
|
18
|
+
const id = ++request.current;
|
|
19
|
+
setLoading(true);
|
|
20
|
+
setError('');
|
|
21
|
+
try {
|
|
22
|
+
if (file && file.size > maxBytes)
|
|
23
|
+
throw new Error('Manifest exceeds the size limit.');
|
|
24
|
+
const address = file ? undefined : safeUrl(url.trim());
|
|
25
|
+
if (!file && !address)
|
|
26
|
+
throw new Error('Enter an HTTP(S) manifest URL.');
|
|
27
|
+
const data = file
|
|
28
|
+
? { text: await file.text(), url: undefined }
|
|
29
|
+
: await loadManifest(address);
|
|
30
|
+
if (new TextEncoder().encode(data.text).length > maxBytes)
|
|
31
|
+
throw new Error('Manifest exceeds the size limit.');
|
|
32
|
+
const parsed = await parse(data.text);
|
|
33
|
+
if (id === request.current)
|
|
34
|
+
onChange({ ...parsed, name: file?.name ?? address, url: data.url });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (id === request.current)
|
|
38
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
if (id === request.current)
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return (_jsxs("section", { className: `bh-selector ${className}`, "aria-label": "Select manifest", "aria-busy": loading, children: [_jsxs("div", { className: "bh-controls", children: [_jsx("input", { ref: fileInput, type: "file", accept: ".json,application/json", hidden: true, onChange: (event) => {
|
|
46
|
+
const file = event.currentTarget.files?.[0];
|
|
47
|
+
event.currentTarget.value = '';
|
|
48
|
+
if (file)
|
|
49
|
+
void open(file);
|
|
50
|
+
} }), _jsx(Button, { onClick: () => fileInput.current?.click(), children: "Open manifest file" }), _jsx(Input, { value: url, onChange: setUrl, placeholder: "https://\u2026/bloodhound.manifest.json", "aria-label": "Manifest URL" }), _jsx(Button, { disabled: !url.trim(), onClick: () => void open(), children: "Load URL" })] }), loading ? _jsx("p", { role: "status", children: "Loading manifest\u2026" }) : null, error ? _jsx("p", { role: "alert", children: error }) : null] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Manifest, type ProjectWorkspace, type RecordRow, type TextLoader } from '@bloodhound-rs/manifest';
|
|
2
|
+
import { type ManifestComponents } from './components.js';
|
|
3
|
+
export interface ManifestViewerProps {
|
|
4
|
+
/** Already parsed report; replace this object when the build changes. */
|
|
5
|
+
manifest: Manifest;
|
|
6
|
+
components?: Partial<ManifestComponents>;
|
|
7
|
+
/** Base URL for relative source map references. */
|
|
8
|
+
manifestUrl?: string;
|
|
9
|
+
workspace?: ProjectWorkspace;
|
|
10
|
+
loadText?: TextLoader;
|
|
11
|
+
notices?: readonly string[];
|
|
12
|
+
pageSize?: number;
|
|
13
|
+
className?: string;
|
|
14
|
+
onSelect?: (row: RecordRow) => void;
|
|
15
|
+
}
|
|
16
|
+
/** Display only: no file picker, manifest request, router or application provider. */
|
|
17
|
+
export declare function ManifestViewer(props: ManifestViewerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useDeferredValue, useMemo, useState } from 'react';
|
|
3
|
+
import { ManifestIndex, OriginalSources, sourceName, } from '@bloodhound-rs/manifest';
|
|
4
|
+
import { defaultComponents } from './components.js';
|
|
5
|
+
import { SourcePreview } from './SourcePreview.js';
|
|
6
|
+
/** Display only: no file picker, manifest request, router or application provider. */
|
|
7
|
+
export function ManifestViewer(props) {
|
|
8
|
+
const { manifest, manifestUrl, workspace, loadText } = props;
|
|
9
|
+
const [session, setSession] = useState({
|
|
10
|
+
manifest,
|
|
11
|
+
manifestUrl,
|
|
12
|
+
workspace,
|
|
13
|
+
loadText,
|
|
14
|
+
version: 0,
|
|
15
|
+
});
|
|
16
|
+
// Reset all selection, filters and source requests together when the host changes reports.
|
|
17
|
+
if (session.manifest !== manifest ||
|
|
18
|
+
session.manifestUrl !== manifestUrl ||
|
|
19
|
+
session.workspace !== workspace ||
|
|
20
|
+
session.loadText !== loadText) {
|
|
21
|
+
setSession({ manifest, manifestUrl, workspace, loadText, version: session.version + 1 });
|
|
22
|
+
}
|
|
23
|
+
return _jsx(ViewerSession, { ...props }, session.version);
|
|
24
|
+
}
|
|
25
|
+
const views = ['findings', 'sources', 'dependencies', 'gaps'];
|
|
26
|
+
const groupings = ['type', 'source', 'folder', 'entry'];
|
|
27
|
+
const initialFilters = {
|
|
28
|
+
view: 'findings',
|
|
29
|
+
query: '',
|
|
30
|
+
severity: '',
|
|
31
|
+
confidence: '',
|
|
32
|
+
grouping: 'type',
|
|
33
|
+
group: null,
|
|
34
|
+
includeDependencies: false,
|
|
35
|
+
};
|
|
36
|
+
function ViewerSession({ manifest, components, manifestUrl, workspace, loadText, notices, pageSize = 50, className = '', onSelect, }) {
|
|
37
|
+
const ui = { ...defaultComponents, ...components };
|
|
38
|
+
const { Button, Input, Select, Table, Tag } = ui;
|
|
39
|
+
const index = useMemo(() => new ManifestIndex(manifest), [manifest]);
|
|
40
|
+
const originals = useMemo(() => new OriginalSources(manifestUrl, workspace, loadText), [manifestUrl, workspace, loadText]);
|
|
41
|
+
const [filters, setFilters] = useState(initialFilters);
|
|
42
|
+
const [selected, setSelected] = useState();
|
|
43
|
+
const [page, setPage] = useState(0);
|
|
44
|
+
const query = useDeferredValue(filters.query);
|
|
45
|
+
const baseRows = useMemo(() => index.baseRows({ ...filters, query }), [index, filters.view, query, filters.severity, filters.confidence]);
|
|
46
|
+
const groups = useMemo(() => index.groups(baseRows, filters.grouping), [index, baseRows, filters.grouping]);
|
|
47
|
+
const rows = useMemo(() => index.filter(baseRows, filters), [index, baseRows, filters.group, filters.grouping, filters.includeDependencies]);
|
|
48
|
+
const size = Math.max(1, Math.floor(pageSize));
|
|
49
|
+
const pages = Math.max(1, Math.ceil(rows.length / size));
|
|
50
|
+
const activePage = Math.min(page, pages - 1);
|
|
51
|
+
const row = selected ? index.byId.get(selected) : undefined;
|
|
52
|
+
function filter(patch) {
|
|
53
|
+
setFilters((value) => ({ ...value, ...patch }));
|
|
54
|
+
setPage(0);
|
|
55
|
+
setSelected(undefined);
|
|
56
|
+
}
|
|
57
|
+
function select(row) {
|
|
58
|
+
setSelected(row.id);
|
|
59
|
+
onSelect?.(row);
|
|
60
|
+
}
|
|
61
|
+
function selectSource(id) {
|
|
62
|
+
const row = index.rows.sources.find((row) => row.source === id);
|
|
63
|
+
if (row)
|
|
64
|
+
select(row);
|
|
65
|
+
}
|
|
66
|
+
const columns = [
|
|
67
|
+
{
|
|
68
|
+
title: 'Record',
|
|
69
|
+
dataIndex: 'title',
|
|
70
|
+
render: (_, row) => _jsx(Button, { onClick: () => select(row), children: row.title }),
|
|
71
|
+
},
|
|
72
|
+
{ title: 'Type', dataIndex: 'type', render: (_, row) => row.type },
|
|
73
|
+
{ title: 'Status', dataIndex: 'status', render: (_, row) => _jsx(Tag, { children: row.status }) },
|
|
74
|
+
{
|
|
75
|
+
title: 'Source',
|
|
76
|
+
dataIndex: 'source',
|
|
77
|
+
render: (_, row) => (row.source ? sourceName(index.sources.get(row.source)) : 'Scan-wide'),
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const source = row?.source ? index.sources.get(row.source) : undefined;
|
|
81
|
+
const statusOptions = [...new Set(index.rows[filters.view].map((row) => row.status))];
|
|
82
|
+
return (_jsxs("section", { className: `bh-viewer ${className}`, "aria-label": "Bloodhound manifest report", children: [_jsxs("header", { className: "bh-heading", children: [_jsx("strong", { children: "Manifest report" }), _jsx(Tag, { children: manifest.status }), _jsxs("span", { children: ["Scanner ", manifest.scannerVersion, " \u00B7 ", manifest.dependencyMode] })] }), _jsx("p", { className: "bh-muted", children: "Static findings are candidates. Unresolved references and coverage gaps describe code the scan could not fully analyze." }), notices?.map((notice) => (_jsx("p", { role: "status", children: notice }, notice))), _jsx("nav", { className: "bh-controls", "aria-label": "Record categories", children: views.map((view) => (_jsxs(Button, { disabled: filters.view === view, onClick: () => filter({ ...initialFilters, view }), children: [view, " (", index.rows[view].length, ")"] }, view))) }), _jsxs("div", { className: "bh-controls", children: [_jsx(Input, { value: filters.query, onChange: (query) => filter({ query }), placeholder: "Search records, paths or code", "aria-label": "Search records" }), _jsx(Select, { value: filters.severity, onChange: (severity) => filter({ severity, group: null }), "aria-label": "Status", options: [
|
|
83
|
+
{ label: 'All statuses', value: '' },
|
|
84
|
+
...statusOptions.map((value) => ({ label: value, value })),
|
|
85
|
+
] }), filters.view === 'findings' ? (_jsx(Select, { value: filters.confidence, onChange: (confidence) => filter({ confidence, group: null }), "aria-label": "Confidence", options: [
|
|
86
|
+
{ label: 'All confidence levels', value: '' },
|
|
87
|
+
{ label: 'High', value: 'high' },
|
|
88
|
+
{ label: 'Medium', value: 'medium' },
|
|
89
|
+
] })) : null, _jsx(Select, { value: filters.grouping, onChange: (value) => filter({ grouping: value, group: null }), "aria-label": "Group by", options: groupings.map((value) => ({ label: `By ${value}`, value })) })] }), _jsxs("div", { className: "bh-layout", children: [_jsxs("aside", { className: "bh-groups", "aria-label": "Record groups", children: [_jsxs(Button, { disabled: filters.group === null, onClick: () => filter({ group: null }), children: ["All (", baseRows.length, ")"] }), groups.map((group) => (_jsxs(Button, { disabled: filters.group === group.id, onClick: () => filter({ group: group.id }), children: [group.name, " (", group.count, ")"] }, group.id))), filters.grouping === 'source' ? (_jsx(Button, { onClick: () => filter({ includeDependencies: !filters.includeDependencies }), children: filters.includeDependencies ? 'Exclude dependencies' : 'Include dependencies' })) : null] }), _jsxs("div", { className: "bh-results", children: [_jsx("div", { className: "bh-table-scroll", children: _jsx(Table, { columns: columns, data: rows.slice(activePage * size, (activePage + 1) * size), rowKey: "id", pagination: false }) }), rows.length === 0 ? _jsx("p", { role: "status", children: "No matching records." }) : null, _jsxs("div", { className: "bh-controls", "aria-label": "Pagination", children: [_jsx(Button, { disabled: activePage === 0, onClick: () => setPage(activePage - 1), children: "Previous" }), _jsxs("span", { children: [activePage + 1, " / ", pages, " \u00B7 ", rows.length, " records"] }), _jsx(Button, { disabled: activePage + 1 >= pages, onClick: () => setPage(activePage + 1), children: "Next" })] }), row ? (_jsxs("section", { className: "bh-detail", "aria-label": "Record details", children: [_jsxs("div", { className: "bh-heading", children: [_jsx("strong", { children: row.title }), _jsx(Button, { onClick: () => setSelected(undefined), children: "Close details" })] }), _jsxs("p", { children: [row.type, " \u00B7 ", row.status, row.confidence ? ` · ${row.confidence} confidence` : ''] }), source ? _jsx("p", { className: "bh-path", children: source.file ?? source.url ?? source.id }) : null, 'location' in row.data && row.data.location ? (_jsxs("p", { children: ["Line ", row.data.location.line, ", column ", row.data.location.column] })) : null, 'detail' in row.data && row.data.detail ? _jsx("p", { children: row.data.detail }) : null, 'snippet' in row.data ? _jsx("pre", { className: "bh-snippet", children: row.data.snippet }) : null, source && source.chain.length > 0 ? (_jsxs("div", { className: "bh-controls", "aria-label": "Entry chain", children: ["Entry chain:", ' ', source.chain.map((id) => (_jsx(Button, { onClick: () => selectSource(id), children: sourceName(index.sources.get(id)) }, id)))] })) : null, source ? (_jsxs("div", { className: "bh-controls", "aria-label": "Dependencies", children: ["Dependencies:", ' ', [...(index.children.get(source.id) ?? [])].map((id) => (_jsx(Button, { onClick: () => selectSource(id), children: sourceName(index.sources.get(id)) }, id)))] })) : null, _jsx(SourcePreview, { index: index, row: row, originals: originals, components: ui, customLoader: Boolean(loadText) }, row.id), _jsxs("details", { children: [_jsx("summary", { children: "Record JSON" }), _jsx("pre", { className: "bh-snippet", children: JSON.stringify(row.data, null, 2) })] })] })) : null] })] })] }));
|
|
90
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ManifestIndex, type OriginalSources, type RecordRow } from '@bloodhound-rs/manifest';
|
|
2
|
+
import type { ManifestComponents } from './components.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
index: ManifestIndex;
|
|
5
|
+
row: RecordRow;
|
|
6
|
+
originals: OriginalSources;
|
|
7
|
+
components: ManifestComponents;
|
|
8
|
+
customLoader: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function SourcePreview({ index, row, originals, components, customLoader }: Props): import("react").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { viewerTarget, } from '@bloodhound-rs/manifest';
|
|
4
|
+
function Preview({ load, components, }) {
|
|
5
|
+
const [result, setResult] = useState({ status: 'loading' });
|
|
6
|
+
const [attempt, setAttempt] = useState(0);
|
|
7
|
+
const { Button, SourceCode } = components;
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
let active = true;
|
|
10
|
+
setResult({ status: 'loading' });
|
|
11
|
+
void load(attempt > 0).then((value) => {
|
|
12
|
+
if (active)
|
|
13
|
+
setResult(value);
|
|
14
|
+
}, (error) => {
|
|
15
|
+
if (active)
|
|
16
|
+
setResult({
|
|
17
|
+
status: 'error',
|
|
18
|
+
error: error instanceof Error ? error.message : String(error),
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
return () => {
|
|
22
|
+
active = false;
|
|
23
|
+
};
|
|
24
|
+
}, [load, attempt]);
|
|
25
|
+
if (result.status === 'loading')
|
|
26
|
+
return _jsx("p", { role: "status", children: "Loading source\u2026" });
|
|
27
|
+
if (result.status === 'error')
|
|
28
|
+
return (_jsxs("div", { role: "alert", children: [_jsx("p", { children: result.error }), _jsx(Button, { onClick: () => setAttempt((value) => value + 1), children: "Retry" })] }));
|
|
29
|
+
return (_jsxs("section", { "aria-label": "Source preview", children: [_jsxs("p", { className: "bh-path", children: [result.path, result.location ? `:${result.location.line}:${result.location.column}` : '', result.origin ? ` · ${result.origin}` : ''] }), _jsx(SourceCode, { text: result.text, path: result.path, location: result.location })] }));
|
|
30
|
+
}
|
|
31
|
+
export function SourcePreview({ index, row, originals, components, customLoader }) {
|
|
32
|
+
const { Button, Input } = components;
|
|
33
|
+
const source = row.source ? index.sources.get(row.source) : undefined;
|
|
34
|
+
const location = 'location' in row.data ? row.data.location : undefined;
|
|
35
|
+
const target = viewerTarget(index, row, originals.workspace, customLoader);
|
|
36
|
+
const [link, setLink] = useState('');
|
|
37
|
+
const [error, setError] = useState('');
|
|
38
|
+
const [load, setLoad] = useState();
|
|
39
|
+
const [reference, setReference] = useState(() => source && originals.reference(source));
|
|
40
|
+
const hasPosition = source && location;
|
|
41
|
+
function showOriginal() {
|
|
42
|
+
if (!source || !location)
|
|
43
|
+
return;
|
|
44
|
+
setLoad(() => async (retry) => {
|
|
45
|
+
await originals.preview(row.id, source, location, retry);
|
|
46
|
+
return originals.previews.get(row.id);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function associate() {
|
|
50
|
+
if (!source)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
originals.link(source, link);
|
|
54
|
+
setReference(originals.reference(source));
|
|
55
|
+
setError('');
|
|
56
|
+
showOriginal();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return (_jsxs("div", { className: "bh-source-preview", children: [_jsxs("div", { className: "bh-controls", children: [target ? (_jsx(Button, { onClick: () => setLoad(() => async () => {
|
|
63
|
+
const data = await originals.loadText(target.url, {
|
|
64
|
+
kind: 'source',
|
|
65
|
+
file: target.source.file,
|
|
66
|
+
limit: 8 * 1024 * 1024,
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
status: 'ready',
|
|
70
|
+
text: data.text,
|
|
71
|
+
path: target.source.file ?? data.url,
|
|
72
|
+
location: target.location,
|
|
73
|
+
};
|
|
74
|
+
}), children: "Preview generated code" })) : null, reference && hasPosition ? (_jsx(Button, { onClick: showOriginal, children: "Preview original source" })) : null] }), hasPosition ? (_jsxs("details", { children: [_jsx("summary", { children: "Source map association" }), reference ? (_jsx("p", { className: "bh-path", children: reference.file ?? reference.url })) : (_jsx("p", { children: "No source map linked." })), _jsxs("div", { className: "bh-controls", children: [_jsx(Input, { value: link, onChange: setLink, placeholder: "Source map URL or project path", "aria-label": "Source map URL or project path" }), _jsx(Button, { onClick: associate, disabled: !link.trim(), children: "Link source map" }), reference ? (_jsx(Button, { onClick: () => {
|
|
75
|
+
originals.unlink(source);
|
|
76
|
+
setReference(undefined);
|
|
77
|
+
setLoad(undefined);
|
|
78
|
+
setError('');
|
|
79
|
+
setLink('');
|
|
80
|
+
}, children: "Unlink" })) : null] }), error ? _jsx("p", { role: "alert", children: error }) : null] })) : null, load ? _jsx(Preview, { load: load, components: components }) : null] }));
|
|
81
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { JSXElementConstructor, CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { type Location, type RecordRow } from '@bloodhound-rs/manifest';
|
|
3
|
+
interface ControlProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
style?: CSSProperties;
|
|
6
|
+
'aria-label'?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ButtonProps extends ControlProps {
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
onClick?: () => void;
|
|
12
|
+
}
|
|
13
|
+
export interface InputProps extends ControlProps {
|
|
14
|
+
value: string;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
onChange: (value: string) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface SelectProps extends ControlProps {
|
|
19
|
+
value: string;
|
|
20
|
+
options: {
|
|
21
|
+
label: string;
|
|
22
|
+
value: string;
|
|
23
|
+
}[];
|
|
24
|
+
onChange: (value: string) => void;
|
|
25
|
+
}
|
|
26
|
+
export interface TagProps {
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
export interface TableColumn {
|
|
30
|
+
title: string;
|
|
31
|
+
dataIndex: string;
|
|
32
|
+
render: (value: unknown, record: RecordRow) => ReactNode;
|
|
33
|
+
}
|
|
34
|
+
export interface TableProps {
|
|
35
|
+
data: RecordRow[];
|
|
36
|
+
columns: TableColumn[];
|
|
37
|
+
rowKey: 'id';
|
|
38
|
+
pagination: false;
|
|
39
|
+
}
|
|
40
|
+
export interface SourceCodeProps {
|
|
41
|
+
text: string;
|
|
42
|
+
path?: string;
|
|
43
|
+
/** Lines are 1-based, columns are 0-based. */
|
|
44
|
+
location?: Location;
|
|
45
|
+
}
|
|
46
|
+
/** Pass compatible library exports directly; adapt only components with different props. */
|
|
47
|
+
export interface ManifestComponents {
|
|
48
|
+
Button: JSXElementConstructor<ButtonProps>;
|
|
49
|
+
Input: JSXElementConstructor<InputProps>;
|
|
50
|
+
Select: JSXElementConstructor<SelectProps>;
|
|
51
|
+
Tag: JSXElementConstructor<TagProps>;
|
|
52
|
+
Table: JSXElementConstructor<TableProps>;
|
|
53
|
+
SourceCode: JSXElementConstructor<SourceCodeProps>;
|
|
54
|
+
}
|
|
55
|
+
export declare const defaultComponents: ManifestComponents;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { codeWindow } from '@bloodhound-rs/manifest';
|
|
3
|
+
function Button(props) {
|
|
4
|
+
return _jsx("button", { type: "button", ...props });
|
|
5
|
+
}
|
|
6
|
+
function Input({ onChange, ...props }) {
|
|
7
|
+
return _jsx("input", { ...props, onChange: (event) => onChange(event.target.value) });
|
|
8
|
+
}
|
|
9
|
+
function Select({ options, onChange, ...props }) {
|
|
10
|
+
return (_jsx("select", { ...props, onChange: (event) => onChange(event.target.value), children: options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) }));
|
|
11
|
+
}
|
|
12
|
+
function Tag({ children }) {
|
|
13
|
+
return _jsx("span", { className: "bh-tag", children: children });
|
|
14
|
+
}
|
|
15
|
+
function Table({ columns, data }) {
|
|
16
|
+
return (_jsxs("table", { className: "bh-table", children: [_jsx("thead", { children: _jsx("tr", { children: columns.map((column) => (_jsx("th", { scope: "col", children: column.title }, column.dataIndex))) }) }), _jsx("tbody", { children: data.map((row) => (_jsx("tr", { children: columns.map((column) => (_jsx("td", { children: column.render(undefined, row) }, column.dataIndex))) }, row.id))) })] }));
|
|
17
|
+
}
|
|
18
|
+
function SourceCode({ text, location }) {
|
|
19
|
+
const view = codeWindow(text, location, 25);
|
|
20
|
+
return (_jsxs("div", { className: "bh-code", children: [_jsxs("small", { children: ["Lines ", view.from, "\u2013", view.to, " of ", view.total] }), _jsx("pre", { children: view.lines.map((line) => (_jsxs("div", { "data-highlight": line.inRange || undefined, children: [_jsx("span", { className: "bh-line-number", children: line.number }), line.cutBefore ? '…' : '', line.before, _jsx("mark", { children: line.hit }), line.after, line.cutAfter ? '…' : ''] }, line.number))) })] }));
|
|
21
|
+
}
|
|
22
|
+
export const defaultComponents = {
|
|
23
|
+
Button,
|
|
24
|
+
Input,
|
|
25
|
+
Select,
|
|
26
|
+
Tag,
|
|
27
|
+
Table,
|
|
28
|
+
SourceCode,
|
|
29
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ManifestViewer, type ManifestViewerProps } from './ManifestViewer.js';
|
|
2
|
+
export { ManifestPlayground, type ManifestPlaygroundProps } from './ManifestPlayground.js';
|
|
3
|
+
export { ManifestSelector, type ManifestSelectorProps, type ManifestResource, } from './ManifestSelector.js';
|
|
4
|
+
export { defaultComponents } from './components.js';
|
|
5
|
+
export type { ManifestComponents, ButtonProps, InputProps, SelectProps, TableProps, TableColumn, TagProps, SourceCodeProps, } from './components.js';
|
|
6
|
+
export type { Manifest, TextLoader, TextRequest, TextResource, ProjectWorkspace, RecordRow, } from '@bloodhound-rs/manifest';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@bloodhound-rs/manifest';
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@bloodhound-rs/manifest';
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
.bh-viewer,
|
|
2
|
+
.bh-selector {
|
|
3
|
+
--bh-border: var(--color-border, #dce1e8);
|
|
4
|
+
--bh-muted: var(--color-text-3, #667085);
|
|
5
|
+
--bh-accent: var(--color-primary-light-1, #eef2ff);
|
|
6
|
+
color: inherit;
|
|
7
|
+
font: inherit;
|
|
8
|
+
line-height: 1.5;
|
|
9
|
+
min-width: 0;
|
|
10
|
+
}
|
|
11
|
+
.bh-controls,
|
|
12
|
+
.bh-heading {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
flex-wrap: wrap;
|
|
16
|
+
gap: 8px;
|
|
17
|
+
margin-block: 12px;
|
|
18
|
+
}
|
|
19
|
+
.bh-selector input:not([type='file']),
|
|
20
|
+
.bh-viewer input {
|
|
21
|
+
min-width: 220px;
|
|
22
|
+
}
|
|
23
|
+
.bh-layout {
|
|
24
|
+
display: grid;
|
|
25
|
+
grid-template-columns: minmax(160px, 230px) minmax(0, 1fr);
|
|
26
|
+
gap: 20px;
|
|
27
|
+
}
|
|
28
|
+
.bh-groups {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
align-items: stretch;
|
|
32
|
+
gap: 6px;
|
|
33
|
+
overflow: auto;
|
|
34
|
+
max-height: 65vh;
|
|
35
|
+
}
|
|
36
|
+
.bh-groups > * {
|
|
37
|
+
text-align: left;
|
|
38
|
+
overflow-wrap: anywhere;
|
|
39
|
+
flex-shrink: 0;
|
|
40
|
+
white-space: normal;
|
|
41
|
+
}
|
|
42
|
+
.bh-results {
|
|
43
|
+
min-width: 0;
|
|
44
|
+
}
|
|
45
|
+
.bh-table-scroll {
|
|
46
|
+
overflow-x: auto;
|
|
47
|
+
}
|
|
48
|
+
.bh-table {
|
|
49
|
+
width: 100%;
|
|
50
|
+
border-collapse: collapse;
|
|
51
|
+
text-align: left;
|
|
52
|
+
}
|
|
53
|
+
.bh-table th,
|
|
54
|
+
.bh-table td {
|
|
55
|
+
padding: 10px;
|
|
56
|
+
border-bottom: 1px solid var(--bh-border);
|
|
57
|
+
overflow-wrap: anywhere;
|
|
58
|
+
}
|
|
59
|
+
.bh-table td {
|
|
60
|
+
max-width: 400px;
|
|
61
|
+
}
|
|
62
|
+
.bh-tag {
|
|
63
|
+
display: inline-block;
|
|
64
|
+
padding: 2px 8px;
|
|
65
|
+
border: 1px solid var(--bh-border);
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
}
|
|
68
|
+
.bh-detail {
|
|
69
|
+
border-top: 1px solid var(--bh-border);
|
|
70
|
+
padding-top: 12px;
|
|
71
|
+
}
|
|
72
|
+
.bh-path {
|
|
73
|
+
overflow-wrap: anywhere;
|
|
74
|
+
}
|
|
75
|
+
.bh-muted,
|
|
76
|
+
.bh-line-number {
|
|
77
|
+
color: var(--bh-muted);
|
|
78
|
+
}
|
|
79
|
+
.bh-code pre,
|
|
80
|
+
.bh-snippet {
|
|
81
|
+
overflow: auto;
|
|
82
|
+
max-height: 500px;
|
|
83
|
+
padding: 12px;
|
|
84
|
+
border: 1px solid var(--bh-border);
|
|
85
|
+
border-radius: 4px;
|
|
86
|
+
font: 13px/1.6 monospace;
|
|
87
|
+
}
|
|
88
|
+
.bh-snippet {
|
|
89
|
+
white-space: pre-wrap;
|
|
90
|
+
overflow-wrap: anywhere;
|
|
91
|
+
}
|
|
92
|
+
.bh-line-number {
|
|
93
|
+
display: inline-block;
|
|
94
|
+
min-width: 5ch;
|
|
95
|
+
margin-right: 16px;
|
|
96
|
+
user-select: none;
|
|
97
|
+
text-align: right;
|
|
98
|
+
}
|
|
99
|
+
.bh-code [data-highlight] {
|
|
100
|
+
background: var(--bh-accent);
|
|
101
|
+
}
|
|
102
|
+
.bh-code mark {
|
|
103
|
+
background: #fff1a8;
|
|
104
|
+
color: #1d2939;
|
|
105
|
+
}
|
|
106
|
+
.bh-viewer summary {
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
margin-block: 12px;
|
|
109
|
+
}
|
|
110
|
+
@media (max-width: 700px) {
|
|
111
|
+
.bh-layout {
|
|
112
|
+
grid-template-columns: minmax(0, 1fr);
|
|
113
|
+
}
|
|
114
|
+
.bh-groups {
|
|
115
|
+
max-height: 200px;
|
|
116
|
+
}
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bloodhound-rs/playground",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Embeddable Bloodhound Playground, manifest viewer and resource selector for React",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./styles.css": "./dist/styles.css",
|
|
15
|
+
"./manifest": {
|
|
16
|
+
"types": "./dist/manifest.d.ts",
|
|
17
|
+
"import": "./dist/manifest.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"**/*.css"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@bloodhound-rs/manifest": "0.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typescript": "^5.9.3",
|
|
32
|
+
"@types/react": "^18.3.0"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/Justinidlerz/bloodhound.git",
|
|
37
|
+
"directory": "packages/playground"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"registry": "https://registry.npmjs.org/"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc -p tsconfig.json && node -e \"require('node:fs').copyFileSync('src/styles.css', 'dist/styles.css')\"",
|
|
48
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
49
|
+
}
|
|
6
50
|
}
|