@oss-idle/addon-sdk 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +222 -2
- package/bin/cli.mjs +136 -0
- package/bin/scaffold.mjs +154 -0
- package/dist/bridge/beatmaps.d.ts +57 -0
- package/dist/bridge/beatmaps.d.ts.map +1 -0
- package/dist/bridge/beatmaps.js +65 -0
- package/dist/bridge/character.d.ts +91 -0
- package/dist/bridge/character.d.ts.map +1 -0
- package/dist/bridge/character.js +107 -0
- package/dist/bridge/db.d.ts +44 -0
- package/dist/bridge/db.d.ts.map +1 -0
- package/dist/bridge/db.js +83 -0
- package/dist/bridge/playlists.d.ts +55 -0
- package/dist/bridge/playlists.d.ts.map +1 -0
- package/dist/bridge/playlists.js +85 -0
- package/dist/bridge/scores.d.ts +28 -0
- package/dist/bridge/scores.d.ts.map +1 -0
- package/dist/bridge/scores.js +43 -0
- package/dist/build/index.d.ts +60 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +123 -0
- package/dist/build/plugins.d.ts +88 -0
- package/dist/build/plugins.d.ts.map +1 -0
- package/dist/build/plugins.js +213 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/overlay.d.ts +56 -0
- package/dist/react/overlay.d.ts.map +1 -0
- package/dist/react/overlay.js +63 -0
- package/dist/react/reactAddon.d.ts +37 -0
- package/dist/react/reactAddon.d.ts.map +1 -0
- package/dist/react/reactAddon.js +51 -0
- package/dist/react/useSettings.d.ts +16 -0
- package/dist/react/useSettings.d.ts.map +1 -0
- package/dist/react/useSettings.js +30 -0
- package/dist/runtime/addon.d.ts +83 -0
- package/dist/runtime/addon.d.ts.map +1 -0
- package/dist/runtime/addon.js +108 -0
- package/dist/runtime/buildMeta.d.ts +11 -0
- package/dist/runtime/buildMeta.d.ts.map +1 -0
- package/dist/runtime/buildMeta.js +1 -0
- package/dist/runtime/define.d.ts +23 -0
- package/dist/runtime/define.d.ts.map +1 -0
- package/dist/runtime/define.js +37 -0
- package/dist/runtime/input.d.ts +58 -0
- package/dist/runtime/input.d.ts.map +1 -0
- package/dist/runtime/input.js +119 -0
- package/dist/runtime/settings.d.ts +36 -0
- package/dist/runtime/settings.d.ts.map +1 -0
- package/dist/runtime/settings.js +74 -0
- package/package.json +83 -4
- package/templates/common/README.md +31 -0
- package/templates/common/addon.config.json +6 -0
- package/templates/common/gitignore +3 -0
- package/templates/common/src/env.d.ts +17 -0
- package/templates/common/tsconfig.json +14 -0
- package/templates/plain/src/index.ts +32 -0
- package/templates/react/src/Addon.tsx +44 -0
- package/templates/react/src/Panel.tsx +41 -0
- package/templates/react/src/index.ts +12 -0
- package/templates/react/src/styles.css +61 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted add-on settings.
|
|
3
|
+
*
|
|
4
|
+
* An add-on is remounted on every boot and on every enable/disable, so anything
|
|
5
|
+
* held only in component state is re-entered by the player each time. Settings
|
|
6
|
+
* belong in `localStorage`, which is the same store the client keeps its own
|
|
7
|
+
* options in and survives reloads without touching the game's database.
|
|
8
|
+
*
|
|
9
|
+
* **Keys are namespaced deliberately.** The client writes its settings under
|
|
10
|
+
* bare, unprefixed keys (`db/settings.ts`), and reads them back with a bare
|
|
11
|
+
* `JSON.parse`. An add-on writing a colliding key would corrupt a game setting,
|
|
12
|
+
* so everything here lives under `oss-idle-addon:<namespace>`.
|
|
13
|
+
*
|
|
14
|
+
* Stored values are merged **over** the defaults on load, so adding a setting in
|
|
15
|
+
* a later version does not invalidate what a player already saved, and only keys
|
|
16
|
+
* the defaults still declare are kept - a removed setting cleans itself up.
|
|
17
|
+
*/
|
|
18
|
+
const PREFIX = 'oss-idle-addon:';
|
|
19
|
+
/** {@link PREFIX} before the rename to oss!idle: still read, so saved settings carry over. */
|
|
20
|
+
const LEGACY_PREFIX = 'osu-idle-addon:';
|
|
21
|
+
/**
|
|
22
|
+
* A namespaced settings store.
|
|
23
|
+
*
|
|
24
|
+
* Every operation is guarded: `localStorage` can be unavailable (private
|
|
25
|
+
* browsing, storage disabled) or full, and losing settings must never take the
|
|
26
|
+
* add-on - or the game - down with it.
|
|
27
|
+
*/
|
|
28
|
+
export const createSettings = (namespace, defaults) => {
|
|
29
|
+
const key = `${PREFIX}${namespace}`;
|
|
30
|
+
return {
|
|
31
|
+
key,
|
|
32
|
+
load() {
|
|
33
|
+
try {
|
|
34
|
+
const raw = localStorage.getItem(key) ?? localStorage.getItem(`${LEGACY_PREFIX}${namespace}`);
|
|
35
|
+
if (!raw)
|
|
36
|
+
return { ...defaults };
|
|
37
|
+
const stored = JSON.parse(raw);
|
|
38
|
+
if (!stored || typeof stored !== 'object')
|
|
39
|
+
return { ...defaults };
|
|
40
|
+
// Take only keys the defaults still declare, and only when the
|
|
41
|
+
// stored value has the same shape - a hand-edited or stale entry
|
|
42
|
+
// must not put a string where a number is expected.
|
|
43
|
+
const merged = { ...defaults };
|
|
44
|
+
for (const name of Object.keys(defaults)) {
|
|
45
|
+
const value = stored[name];
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
if (typeof value !== typeof defaults[name])
|
|
49
|
+
continue;
|
|
50
|
+
merged[name] = value;
|
|
51
|
+
}
|
|
52
|
+
return merged;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { ...defaults };
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
save(value) {
|
|
59
|
+
try {
|
|
60
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Full or unavailable storage: the add-on still works, it just
|
|
64
|
+
// will not remember. Not worth interrupting the player over.
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
clear() {
|
|
68
|
+
try {
|
|
69
|
+
localStorage.removeItem(key);
|
|
70
|
+
}
|
|
71
|
+
catch { /* nothing to do */ }
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-idle/addon-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"description": "oss!idle Addon SDK - build oss!idle add-ons in TypeScript. An Addon base class with automatic teardown, shadow-root React overlays, bridges to the game's database and beatmap store, and a bundler that emits the single-file ES module the add-on runtime loads.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"osu",
|
|
9
|
+
"oss-idle",
|
|
10
|
+
"addon",
|
|
11
|
+
"sdk",
|
|
12
|
+
"plugin",
|
|
13
|
+
"typescript",
|
|
14
|
+
"esbuild"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/oss-idle/addon-sdk#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/oss-idle/addon-sdk.git",
|
|
20
|
+
"directory": "packages/addon-sdk"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/oss-idle/addon-sdk/issues"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./react": {
|
|
36
|
+
"types": "./dist/react/index.d.ts",
|
|
37
|
+
"default": "./dist/react/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./build": {
|
|
40
|
+
"types": "./dist/build/index.d.ts",
|
|
41
|
+
"default": "./dist/build/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json",
|
|
44
|
+
"./scaffold": "./bin/scaffold.mjs"
|
|
45
|
+
},
|
|
46
|
+
"bin": {
|
|
47
|
+
"oss-idle-addon": "bin/cli.mjs"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"bin",
|
|
52
|
+
"templates",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
],
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsc -p tsconfig.json",
|
|
61
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
62
|
+
"prepublishOnly": "npm run build"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"esbuild": "^0.24.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"react": "^18.3.1",
|
|
69
|
+
"react-dom": "^18.3.1"
|
|
70
|
+
},
|
|
71
|
+
"peerDependenciesMeta": {
|
|
72
|
+
"react": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"react-dom": {
|
|
76
|
+
"optional": true
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@types/node": "^26.4.1",
|
|
81
|
+
"@types/react": "^18.3.12",
|
|
82
|
+
"@types/react-dom": "^18.3.1",
|
|
83
|
+
"typescript": "^5.6.3"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# __ADDON_NAME__
|
|
2
|
+
|
|
3
|
+
An add-on for [oss!idle](https://github.com/oss-idle/oss-idle), built with the
|
|
4
|
+
[oss!idle Addon SDK](https://github.com/oss-idle/addon-sdk).
|
|
5
|
+
|
|
6
|
+
## Build
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install
|
|
10
|
+
npm run build
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That emits `__OUTFILE__` - a single self-contained ES module. Paste its contents
|
|
14
|
+
into the in-game add-on editor (Options → Add-ons → Manage) and enable it.
|
|
15
|
+
|
|
16
|
+
`npm run dev` rebuilds on save; re-paste to see changes.
|
|
17
|
+
|
|
18
|
+
## How an add-on runs
|
|
19
|
+
|
|
20
|
+
The game imports your built file as an ES module from a Blob URL and calls its
|
|
21
|
+
`mount` / `unmount` exports. Nothing is injected and nothing resolves at load
|
|
22
|
+
time, so every dependency is bundled into that one file.
|
|
23
|
+
|
|
24
|
+
`unmount` has to genuinely undo everything, because add-ons are enabled,
|
|
25
|
+
disabled, updated and removed without a page reload. The SDK's `Addon` base
|
|
26
|
+
class records teardown for every listener, timer and overlay it hands you and
|
|
27
|
+
reverses it for you.
|
|
28
|
+
|
|
29
|
+
## Licence
|
|
30
|
+
|
|
31
|
+
__LICENSE__
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Stylesheets are inlined as strings and injected into the overlay's shadow root. */
|
|
2
|
+
declare module '*.css' {
|
|
3
|
+
const css: string;
|
|
4
|
+
export default css;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Workers are inlined into the single-file bundle and spawned from a Blob URL. */
|
|
8
|
+
declare module 'worker:*' {
|
|
9
|
+
const createWorker: () => Worker;
|
|
10
|
+
export default createWorker;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Binary assets inlined as base64 (e.g. `import wasm from './x.wasm?base64'`). */
|
|
14
|
+
declare module '*?base64' {
|
|
15
|
+
const base64: string;
|
|
16
|
+
export default base64;
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"],
|
|
5
|
+
"module": "ES2022",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"noEmit": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* __ADDON_NAME__
|
|
3
|
+
*
|
|
4
|
+
* oss!idle loads an add-on by importing its built source as an ES module and
|
|
5
|
+
* calling `mount` / `unmount`. That pair is the whole contract - `defineAddon`
|
|
6
|
+
* produces it from the class below.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Addon, defineAddon, gameDb } from '@oss-idle/addon-sdk';
|
|
10
|
+
|
|
11
|
+
class __CLASS__ extends Addon {
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
super({ name: '__ADDON_NAME__' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected async onMount(): Promise<void> {
|
|
18
|
+
// The client opens its database lazily, so wait before querying it.
|
|
19
|
+
await gameDb.ready();
|
|
20
|
+
|
|
21
|
+
const played = await gameDb.value<number>('SELECT COUNT(*) FROM score');
|
|
22
|
+
this.log.info(`hello! you have ${played ?? 0} scores stored locally`);
|
|
23
|
+
|
|
24
|
+
// Anything registered through `this.on` / `this.interval` / `this.own`
|
|
25
|
+
// is torn down automatically when the add-on is disabled or updated.
|
|
26
|
+
this.on(window, 'keydown', event => {
|
|
27
|
+
if (event.key === 'F9') this.log.info('F9');
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const { mount, unmount } = defineAddon(() => new __CLASS__());
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* __ADDON_NAME__
|
|
3
|
+
*
|
|
4
|
+
* The panel is mounted for the add-on's whole life and only *hidden* when
|
|
5
|
+
* closed. Returning `null` instead would unmount the React tree, discarding its
|
|
6
|
+
* state and running cleanup - so anything the overlay had started would be
|
|
7
|
+
* thrown away the moment the player closed it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ReactNode } from 'react';
|
|
11
|
+
import { ReactAddon } from '@oss-idle/addon-sdk/react';
|
|
12
|
+
import Panel from './Panel.js';
|
|
13
|
+
import styles from './styles.css';
|
|
14
|
+
|
|
15
|
+
const TOGGLE = 'Ctrl+Shift+__KEY__';
|
|
16
|
+
|
|
17
|
+
export class __CLASS__ extends ReactAddon {
|
|
18
|
+
|
|
19
|
+
private open = false;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super({ name: '__ADDON_NAME__', overlayId: '__SLUG__', styles });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected override async onReady(): Promise<void> {
|
|
26
|
+
// Registered in the capture phase, ahead of the game's own handlers, so
|
|
27
|
+
// the combo never reaches gameplay.
|
|
28
|
+
this.own(this.hotkeys.register(TOGGLE, () => this.toggle(), { allowInText: true }));
|
|
29
|
+
this.own(this.hotkeys.register('Escape', () => {
|
|
30
|
+
if (this.open) this.toggle();
|
|
31
|
+
}, { allowInText: true }));
|
|
32
|
+
|
|
33
|
+
this.log.info(`ready - press ${TOGGLE} to open`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private toggle(): void {
|
|
37
|
+
this.open = !this.open;
|
|
38
|
+
this.refresh();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected render(): ReactNode {
|
|
42
|
+
return <Panel open={this.open} version={this.meta.version ?? ''} onClose={() => this.toggle()} />;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { gameCharacter, type CharacterRow } from '@oss-idle/addon-sdk';
|
|
3
|
+
|
|
4
|
+
export default function Panel({
|
|
5
|
+
open,
|
|
6
|
+
version,
|
|
7
|
+
onClose,
|
|
8
|
+
}: {
|
|
9
|
+
open: boolean;
|
|
10
|
+
version: string;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
}) {
|
|
13
|
+
const [character, setCharacter] = useState<CharacterRow>();
|
|
14
|
+
|
|
15
|
+
// Re-read when opened: the client writes skill levels back to this row after
|
|
16
|
+
// every play, so a value read once at boot goes stale.
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (open) void gameCharacter.live().then(setCharacter);
|
|
19
|
+
}, [open]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div style={{ display: open ? 'contents' : 'none' }} aria-hidden={!open}>
|
|
23
|
+
<div className="backdrop" onClick={onClose} />
|
|
24
|
+
<div className="panel" role="dialog" aria-label="__ADDON_NAME__">
|
|
25
|
+
<header>
|
|
26
|
+
<h1>__ADDON_NAME__</h1>
|
|
27
|
+
<span className="spacer" />
|
|
28
|
+
<span className="hint">v{version}</span>
|
|
29
|
+
<button onClick={onClose}>Close</button>
|
|
30
|
+
</header>
|
|
31
|
+
|
|
32
|
+
<div className="body">
|
|
33
|
+
{character
|
|
34
|
+
? <p>Playing as <strong>{character.name}</strong> (level {character.overallLevel}).</p>
|
|
35
|
+
: <p>Looking for your character...</p>}
|
|
36
|
+
<p>Edit <code>src/Panel.tsx</code> to build your add-on.</p>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* __ADDON_NAME__
|
|
3
|
+
*
|
|
4
|
+
* oss!idle loads an add-on by importing its built source as an ES module and
|
|
5
|
+
* calling `mount` / `unmount`. That pair is the whole contract - `defineAddon`
|
|
6
|
+
* produces it from the class in `Addon.tsx`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { defineAddon } from '@oss-idle/addon-sdk';
|
|
10
|
+
import { __CLASS__ } from './Addon.js';
|
|
11
|
+
|
|
12
|
+
export const { mount, unmount } = defineAddon(() => new __CLASS__());
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Rendered inside a shadow root, so these selectors cannot reach the game and
|
|
3
|
+
* the game's stylesheet cannot reach them.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
:host, * { box-sizing: border-box; }
|
|
7
|
+
|
|
8
|
+
.backdrop {
|
|
9
|
+
position: absolute;
|
|
10
|
+
inset: 0;
|
|
11
|
+
background: rgb(8 6 12 / 55%);
|
|
12
|
+
pointer-events: auto;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.panel {
|
|
16
|
+
position: absolute;
|
|
17
|
+
top: 50%;
|
|
18
|
+
left: 50%;
|
|
19
|
+
transform: translate(-50%, -50%);
|
|
20
|
+
width: min(720px, 92vw);
|
|
21
|
+
max-height: 80vh;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
pointer-events: auto;
|
|
25
|
+
|
|
26
|
+
font-family: ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
|
|
27
|
+
font-size: 13px;
|
|
28
|
+
color: #eceaf2;
|
|
29
|
+
background: #16141c;
|
|
30
|
+
border: 1px solid #322c40;
|
|
31
|
+
border-radius: 12px;
|
|
32
|
+
box-shadow: 0 24px 80px rgb(0 0 0 / 60%);
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
header {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 12px;
|
|
40
|
+
padding: 14px 18px;
|
|
41
|
+
background: #1e1a28;
|
|
42
|
+
border-bottom: 1px solid #322c40;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
header h1 { margin: 0; font-size: 15px; font-weight: 600; }
|
|
46
|
+
header .spacer { flex: 1; }
|
|
47
|
+
header .hint { color: #8b83a0; font-size: 11px; }
|
|
48
|
+
|
|
49
|
+
button {
|
|
50
|
+
font: inherit;
|
|
51
|
+
color: #eceaf2;
|
|
52
|
+
background: #2b2438;
|
|
53
|
+
border: 1px solid #3d3550;
|
|
54
|
+
border-radius: 6px;
|
|
55
|
+
padding: 6px 12px;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
}
|
|
58
|
+
button:hover { background: #362d47; }
|
|
59
|
+
|
|
60
|
+
.body { padding: 18px; overflow: auto; line-height: 1.7; }
|
|
61
|
+
code { background: #221d2e; border-radius: 4px; padding: 1px 5px; }
|