clankerbend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEVELOPERS.md +63 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/apps/README.md +11 -0
- package/apps/sticky-notes/README.md +11 -0
- package/apps/sticky-notes/onewhack.manifest.json +74 -0
- package/apps/sticky-notes/package.json +14 -0
- package/apps/sticky-notes/public/app.js +117 -0
- package/apps/sticky-notes/public/index.html +23 -0
- package/apps/sticky-notes/public/styles.css +84 -0
- package/apps/sticky-notes/src/sticky-notes-app.js +413 -0
- package/apps/vim-nav/README.md +126 -0
- package/apps/vim-nav/onewhack.manifest.json +69 -0
- package/apps/vim-nav/package.json +16 -0
- package/apps/vim-nav/public/app.js +276 -0
- package/apps/vim-nav/public/index.html +53 -0
- package/apps/vim-nav/public/styles.css +211 -0
- package/apps/vim-nav/server.mjs +10 -0
- package/apps/vim-nav/src/vim-nav-app.js +221 -0
- package/assets/onewhack.jpg +0 -0
- package/cli.mjs +91 -0
- package/docs/app-lifecycle.md +63 -0
- package/docs/app-manifest.md +130 -0
- package/docs/author-guide.md +126 -0
- package/docs/launcher-profiles.md +50 -0
- package/docs/protocol.md +1935 -0
- package/host/README.md +18 -0
- package/host/src/app-registry.js +315 -0
- package/host/src/codex-desktop-cdp-adapter.js +1826 -0
- package/host/src/codex-desktop-renderer-bridge.js +3536 -0
- package/host/src/index.js +1177 -0
- package/launch/profiles.mjs +93 -0
- package/launch/runtime-paths.mjs +21 -0
- package/package.json +66 -0
- package/scripts/release-npm.mjs +202 -0
- package/server.mjs +58 -0
package/DEVELOPERS.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# OneWhack Developers
|
|
2
|
+
|
|
3
|
+
## Local Commands
|
|
4
|
+
|
|
5
|
+
For protocol-only local testing without Codex Desktop:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx clankerbend codex --mock
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For local development:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm start
|
|
15
|
+
npm run start:mock
|
|
16
|
+
npm test
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`npm test` runs the fast behavior e2e suite through the host and app APIs. To
|
|
20
|
+
exercise the real Codex Desktop surface:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm run test:desktop-real:integration
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The real desktop tests create fresh chats, seed deterministic content, verify
|
|
27
|
+
VimNav against the real transcript DOM, and verify Sticky Notes through native
|
|
28
|
+
toolbar injection, pinned overlay save, runtime file attachment, and prompt
|
|
29
|
+
context visibility.
|
|
30
|
+
|
|
31
|
+
## Useful Docs
|
|
32
|
+
|
|
33
|
+
- `docs/protocol.md`: OneWhack protocol
|
|
34
|
+
- `docs/app-manifest.md`: app manifest format
|
|
35
|
+
- `docs/author-guide.md`: writing OneWhack apps
|
|
36
|
+
- `docs/launcher-profiles.md`: launcher profiles
|
|
37
|
+
- `docs/app-lifecycle.md`: app installation and lifecycle model
|
|
38
|
+
|
|
39
|
+
## Release
|
|
40
|
+
|
|
41
|
+
OneWhack publishes the same package payload under two npm names:
|
|
42
|
+
|
|
43
|
+
- `@onewillai/clankerbend`: scoped canonical package
|
|
44
|
+
- `clankerbend`: short launcher name for `npx clankerbend codex`
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npm test
|
|
48
|
+
npm run test:desktop-real:integration
|
|
49
|
+
npm run release:pack
|
|
50
|
+
npm run release:publish -- --yes
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`release:pack` creates:
|
|
54
|
+
|
|
55
|
+
- `dist/npm/clankerbend-<version>.tgz`
|
|
56
|
+
- `dist/npm/onewillai-clankerbend-<version>.tgz`
|
|
57
|
+
|
|
58
|
+
`release:publish` is guarded by `--yes`. It publishes the scoped package first
|
|
59
|
+
with `--access public`, then publishes the unscoped package. The publish steps
|
|
60
|
+
run interactively so npm can prompt for the configured 2FA method. Pass
|
|
61
|
+
`--otp=<code>` only for accounts that use authenticator-app TOTP codes.
|
|
62
|
+
Provenance is enabled automatically in supported CI providers, or explicitly
|
|
63
|
+
with `--provenance`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OneWill.ai
|
|
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
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# OneWhack
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./assets/onewhack.jpg" alt="OneWhack: mod your clanker with OneWill.ai" width="720">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
OneWhack lets you build Codex Desktop plugins without rebuilding or re-signing
|
|
8
|
+
Codex Desktop itself. It uses [CDP](https://x.com/OpenAIDevs/status/2065226355495895521)
|
|
9
|
+
to provide a more stable API for tasks such as transcript navigation, chat annotation,
|
|
10
|
+
attaching a file to the prompt window, opening the side panel, and so on.
|
|
11
|
+
|
|
12
|
+
DEMO VIDEO TODO
|
|
13
|
+
|
|
14
|
+
OneWhack is an independent [OneWill](https://onewill.ai) project compatible
|
|
15
|
+
with OpenAI Codex Desktop on macOS. It is not affiliated with or endorsed by
|
|
16
|
+
OpenAI (unless... 🥺).
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
You'll need macOS with [Codex Desktop](https://chatgpt.com/codex/)
|
|
21
|
+
and [nodejs](https://nodejs.org/en/download). Then just:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx clankerbend codex
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That starts Codex Desktop with the default OneWhack profile and bundled apps:
|
|
28
|
+
|
|
29
|
+
- **VimNav**: Vim-style transcript navigation, number rails, role jumps, search,
|
|
30
|
+
and a side panel.
|
|
31
|
+
- **Sticky Notes**: select transcript text, click **Add note**, and the
|
|
32
|
+
generated note will (1) create a markdown file and (2) attach itself to the
|
|
33
|
+
current Codex prompt. Inspired by
|
|
34
|
+
[@zats](https://x.com/zats/status/2070492220084326907).
|
|
35
|
+
|
|
36
|
+
## Runtime State
|
|
37
|
+
|
|
38
|
+
OneWhack writes runtime state outside the package:
|
|
39
|
+
|
|
40
|
+
- macOS: `~/Library/Application Support/OneWill/OneWhack`
|
|
41
|
+
|
|
42
|
+
Override with `ONEWILL_ONEWHACK_STATE_DIR`.
|
|
43
|
+
|
|
44
|
+
## Security
|
|
45
|
+
|
|
46
|
+
OneWhack binds to `127.0.0.1` on an OS-assigned ephemeral port. The launcher
|
|
47
|
+
prints the exact host URL, for example `Host: http://127.0.0.1:49152`.
|
|
48
|
+
`/onewhack/*` endpoints require a bearer token by default. For local protocol
|
|
49
|
+
debugging, you can set `ONEWILL_ONEWHACK_DISABLE_AUTH=1`.
|
package/apps/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# OneWhack Apps
|
|
2
|
+
|
|
3
|
+
This directory contains public OneWhack reference apps.
|
|
4
|
+
|
|
5
|
+
- `vim-nav/`: Vim-like transcript navigation for Codex Desktop transcript
|
|
6
|
+
anchors.
|
|
7
|
+
- `sticky-notes/`: selected transcript notes and composer context chips through
|
|
8
|
+
the shared OneWhack host API.
|
|
9
|
+
|
|
10
|
+
The public `apps/` tree should contain standalone, auditable apps without
|
|
11
|
+
project-specific state or action vocabulary.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Sticky Notes
|
|
2
|
+
|
|
3
|
+
Sticky Notes is a public OneWhack reference app for selected transcript text,
|
|
4
|
+
anchored note overlays, composer context chips, and follow-up draft insertion.
|
|
5
|
+
|
|
6
|
+
The app does not inspect or mutate Codex Desktop DOM. It contributes selection
|
|
7
|
+
actions and handles note business logic through the OneWhack host API.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm --prefix apps/sticky-notes test
|
|
11
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"oneWhackVersion": "0.1",
|
|
3
|
+
"appId": "onewill.sticky-notes",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"name": "OneWill Sticky Notes",
|
|
6
|
+
"description": "Public reference app for selected transcript notes and composer context chips.",
|
|
7
|
+
"distribution": {
|
|
8
|
+
"kind": "local",
|
|
9
|
+
"source": ".",
|
|
10
|
+
"integrity": "dev-local",
|
|
11
|
+
"update": {
|
|
12
|
+
"channel": "local"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"entrypoint": {
|
|
16
|
+
"kind": "module",
|
|
17
|
+
"module": "./src/sticky-notes-app.js",
|
|
18
|
+
"factory": "createStickyNotesApp",
|
|
19
|
+
"publicDir": "./public"
|
|
20
|
+
},
|
|
21
|
+
"platform": {
|
|
22
|
+
"os": [
|
|
23
|
+
"darwin",
|
|
24
|
+
"linux",
|
|
25
|
+
"win32"
|
|
26
|
+
],
|
|
27
|
+
"arch": [
|
|
28
|
+
"any"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"capabilities": {
|
|
32
|
+
"panel": true,
|
|
33
|
+
"annotations": true,
|
|
34
|
+
"actions": true,
|
|
35
|
+
"appState": true,
|
|
36
|
+
"selectionActions": true,
|
|
37
|
+
"overlays": true,
|
|
38
|
+
"composerContext": true,
|
|
39
|
+
"composerDraft": true,
|
|
40
|
+
"rendererBridge": false
|
|
41
|
+
},
|
|
42
|
+
"permissions": {
|
|
43
|
+
"transcriptRead": true,
|
|
44
|
+
"transcriptAnnotate": true,
|
|
45
|
+
"transcriptNavigate": true,
|
|
46
|
+
"overlayWrite": true,
|
|
47
|
+
"composerWrite": true,
|
|
48
|
+
"appServerRead": false,
|
|
49
|
+
"appServerApprove": false,
|
|
50
|
+
"appServerRollback": false
|
|
51
|
+
},
|
|
52
|
+
"panel": {
|
|
53
|
+
"title": "Sticky Notes",
|
|
54
|
+
"reloadPolicy": "preserve",
|
|
55
|
+
"preferredWidth": 380
|
|
56
|
+
},
|
|
57
|
+
"lifecycle": {
|
|
58
|
+
"install": {
|
|
59
|
+
"kind": "noop"
|
|
60
|
+
},
|
|
61
|
+
"start": {
|
|
62
|
+
"kind": "in-process"
|
|
63
|
+
},
|
|
64
|
+
"stop": {
|
|
65
|
+
"kind": "in-process"
|
|
66
|
+
},
|
|
67
|
+
"update": {
|
|
68
|
+
"kind": "replace-manifest"
|
|
69
|
+
},
|
|
70
|
+
"remove": {
|
|
71
|
+
"kind": "forget"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "onewhack-sticky-notes",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OneWhack public reference app for transcript range notes and composer context.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node test-onewhack.mjs",
|
|
9
|
+
"test:desktop-real": "node test-desktop-real.mjs"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const APP_ID = "onewill.sticky-notes";
|
|
2
|
+
|
|
3
|
+
const els = {
|
|
4
|
+
subtitle: document.getElementById("subtitle"),
|
|
5
|
+
notes: document.getElementById("notes"),
|
|
6
|
+
userText: document.getElementById("user-text"),
|
|
7
|
+
insertContext: document.getElementById("insert-context")
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const token = readToken();
|
|
11
|
+
let currentState = null;
|
|
12
|
+
|
|
13
|
+
els.insertContext.addEventListener("click", () => runAction("sticky.compose.insertContext", {
|
|
14
|
+
userText: els.userText.value,
|
|
15
|
+
mode: "replace"
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
bootstrap();
|
|
19
|
+
|
|
20
|
+
async function bootstrap() {
|
|
21
|
+
const state = await getJson("/onewhack/state");
|
|
22
|
+
render(state);
|
|
23
|
+
connectEvents();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function render(state) {
|
|
27
|
+
currentState = state;
|
|
28
|
+
const app = (state.apps || []).find((candidate) => candidate.appId === APP_ID);
|
|
29
|
+
const entries = app?.entries || [];
|
|
30
|
+
els.subtitle.textContent = `${entries.length} note${entries.length === 1 ? "" : "s"} queued`;
|
|
31
|
+
els.notes.replaceChildren(...entries.map((entry) => {
|
|
32
|
+
const li = document.createElement("li");
|
|
33
|
+
const title = document.createElement("strong");
|
|
34
|
+
title.textContent = entry.title;
|
|
35
|
+
const summary = document.createElement("div");
|
|
36
|
+
summary.textContent = entry.summary;
|
|
37
|
+
const meta = document.createElement("small");
|
|
38
|
+
meta.textContent = entry.status;
|
|
39
|
+
li.append(title, summary, meta);
|
|
40
|
+
return li;
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runAction(type, payload = {}) {
|
|
45
|
+
const action = {
|
|
46
|
+
actionId: `${type}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
47
|
+
appId: APP_ID,
|
|
48
|
+
type,
|
|
49
|
+
payload,
|
|
50
|
+
requestedAt: new Date().toISOString()
|
|
51
|
+
};
|
|
52
|
+
const result = await postJson(`/onewhack/apps/${encodeURIComponent(APP_ID)}/actions`, { action });
|
|
53
|
+
const state = await getJson("/onewhack/state");
|
|
54
|
+
render(state);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readToken() {
|
|
59
|
+
const params = new URLSearchParams(location.hash.startsWith("#") ? location.hash.slice(1) : location.hash);
|
|
60
|
+
const value = params.get("onewhack_token") || "";
|
|
61
|
+
if (value) history.replaceState(null, "", location.pathname + location.search);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function headers(extra = {}) {
|
|
66
|
+
return {
|
|
67
|
+
...extra,
|
|
68
|
+
...(token ? { authorization: `Bearer ${token}` } : {})
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function connectEvents() {
|
|
73
|
+
if (!token) {
|
|
74
|
+
const events = new EventSource("/onewhack/events");
|
|
75
|
+
events.addEventListener("state", (event) => render(JSON.parse(event.data)));
|
|
76
|
+
events.addEventListener("action", () => getJson("/onewhack/state").then(render));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
fetch("/onewhack/events", { headers: headers() }).then(async (res) => {
|
|
80
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
81
|
+
let buffer = "";
|
|
82
|
+
while (true) {
|
|
83
|
+
const { value, done } = await reader.read();
|
|
84
|
+
if (done) break;
|
|
85
|
+
buffer += value;
|
|
86
|
+
let boundary;
|
|
87
|
+
while ((boundary = buffer.indexOf("\n\n")) >= 0) {
|
|
88
|
+
const chunk = buffer.slice(0, boundary);
|
|
89
|
+
buffer = buffer.slice(boundary + 2);
|
|
90
|
+
const event = chunk.split(/\n/).find((line) => line.startsWith("event: "))?.slice(7);
|
|
91
|
+
const data = chunk.split(/\n/).filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).join("\n");
|
|
92
|
+
if (event === "state" && data) render(JSON.parse(data));
|
|
93
|
+
if (event === "action") getJson("/onewhack/state").then(render);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getJson(url) {
|
|
100
|
+
const res = await fetch(url, { headers: headers() });
|
|
101
|
+
return unwrap(res);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function postJson(url, body) {
|
|
105
|
+
const res = await fetch(url, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: headers({ "content-type": "application/json" }),
|
|
108
|
+
body: JSON.stringify(body)
|
|
109
|
+
});
|
|
110
|
+
return unwrap(res);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function unwrap(res) {
|
|
114
|
+
const json = await res.json();
|
|
115
|
+
if (!json.ok) throw new Error(json.error?.message || `request failed: ${res.status}`);
|
|
116
|
+
return json.data;
|
|
117
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Sticky Notes</title>
|
|
7
|
+
<link rel="stylesheet" href="./styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<header>
|
|
12
|
+
<h1>Sticky Notes</h1>
|
|
13
|
+
<p id="subtitle">Transcript notes</p>
|
|
14
|
+
</header>
|
|
15
|
+
<section class="composer">
|
|
16
|
+
<textarea id="user-text" placeholder="Follow-up request"></textarea>
|
|
17
|
+
<button id="insert-context" type="button">Insert context</button>
|
|
18
|
+
</section>
|
|
19
|
+
<ul id="notes"></ul>
|
|
20
|
+
</main>
|
|
21
|
+
<script type="module" src="./app.js"></script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
body {
|
|
6
|
+
margin: 0;
|
|
7
|
+
background: #fbfaf7;
|
|
8
|
+
color: #24221d;
|
|
9
|
+
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
main {
|
|
13
|
+
display: grid;
|
|
14
|
+
gap: 14px;
|
|
15
|
+
padding: 16px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
header {
|
|
19
|
+
border-bottom: 1px solid #e7dfc4;
|
|
20
|
+
padding-bottom: 10px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
h1 {
|
|
24
|
+
margin: 0;
|
|
25
|
+
font-size: 18px;
|
|
26
|
+
line-height: 1.2;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
p {
|
|
30
|
+
margin: 4px 0 0;
|
|
31
|
+
color: #6c6658;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.composer {
|
|
35
|
+
display: grid;
|
|
36
|
+
gap: 8px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
textarea {
|
|
40
|
+
width: 100%;
|
|
41
|
+
min-height: 86px;
|
|
42
|
+
resize: vertical;
|
|
43
|
+
border: 1px solid #d9cfaa;
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
padding: 10px;
|
|
46
|
+
background: #fff8d8;
|
|
47
|
+
color: #24221d;
|
|
48
|
+
font: inherit;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
button {
|
|
52
|
+
justify-self: start;
|
|
53
|
+
border: 1px solid #bfa643;
|
|
54
|
+
border-radius: 7px;
|
|
55
|
+
background: #f5d75d;
|
|
56
|
+
color: #2c2612;
|
|
57
|
+
padding: 7px 10px;
|
|
58
|
+
font: 650 13px/1 system-ui, sans-serif;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ul {
|
|
63
|
+
display: grid;
|
|
64
|
+
gap: 8px;
|
|
65
|
+
margin: 0;
|
|
66
|
+
padding: 0;
|
|
67
|
+
list-style: none;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
li {
|
|
71
|
+
border: 1px solid #ead87c;
|
|
72
|
+
border-radius: 8px;
|
|
73
|
+
background: #fff0a8;
|
|
74
|
+
padding: 10px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
strong {
|
|
78
|
+
display: block;
|
|
79
|
+
margin-bottom: 4px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
small {
|
|
83
|
+
color: #766b45;
|
|
84
|
+
}
|