@pinkpixel/memi 0.0.0-stage → 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/LICENSE +202 -0
- package/package.json +56 -4
- package/skills/memi-curate/SKILL.md +89 -0
- package/skills/memi-memory/SKILL.md +141 -0
- package/skills/memi-memory/references/examples.md +50 -0
- package/skills/memi-memory/references/rubrics.md +44 -0
- package/skills/memi-setup/SKILL.md +144 -0
- package/ui/css/controls.css +274 -0
- package/ui/css/dialogs.css +145 -0
- package/ui/css/layout.css +237 -0
- package/ui/css/memories.css +168 -0
- package/ui/css/tokens.css +125 -0
- package/ui/favicon.svg +1 -0
- package/ui/fonts/OFL.txt +192 -0
- package/ui/fonts/geist-latin-wght-normal.woff2 +0 -0
- package/ui/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
- package/ui/index.html +75 -0
- package/ui/js/api.js +30 -0
- package/ui/js/confirm.js +29 -0
- package/ui/js/dom.js +49 -0
- package/ui/js/editor.js +133 -0
- package/ui/js/icons.js +29 -0
- package/ui/js/list.js +66 -0
- package/ui/js/main.js +244 -0
- package/ui/js/sidebar.js +85 -0
- package/ui/js/state.js +61 -0
- package/ui/js/toast.js +10 -0
- package/README.md +0 -3
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memi-setup
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: |
|
|
5
|
+
Install, configure, and troubleshoot memi, the local memory system for AI
|
|
6
|
+
agents. Use when the user wants to set up memi, connect its MCP server to a
|
|
7
|
+
client, choose or switch between Ollama and OpenAI embeddings, install the memi
|
|
8
|
+
skills, back up the database, or when memi reports problems such as
|
|
9
|
+
"Could not reach", "not found, try pulling it first", "OPENAI_API_KEY is not
|
|
10
|
+
set", "The embedder changed", "No project detected", or memories missing
|
|
11
|
+
vectors. Also use to interpret `memi doctor` output.
|
|
12
|
+
license: Apache-2.0
|
|
13
|
+
compatibility: claude-code cursor codex gemini-cli opencode
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Setting up memi
|
|
17
|
+
|
|
18
|
+
Start with `memi doctor`. It checks the database, the embedder, the vector index, and project detection, and exits 1 if something is wrong. Most problems show up there with a line that says what to do.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Node.js 22 or newer.
|
|
23
|
+
- One embedder:
|
|
24
|
+
- **Ollama** (the default, runs locally): install Ollama, then `ollama pull nomic-embed-text`.
|
|
25
|
+
- **OpenAI**: an API key in the `OPENAI_API_KEY` environment variable. Memory text is sent to OpenAI to be embedded.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
memi is not published to npm yet, so build it from source:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
node dist/cli/index.js doctor
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Use the absolute path to `dist/cli/index.js` wherever a command below says `<memi>`. Once `@pinkpixel/memi` is published, `npx -y @pinkpixel/memi` will replace `node <path>`.
|
|
38
|
+
|
|
39
|
+
## Connect the MCP server
|
|
40
|
+
|
|
41
|
+
### Claude Code
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
claude mcp add memi -s user -e MEMI_AGENT=claude -- node /absolute/path/to/memi/dist/cli/index.js serve
|
|
45
|
+
claude mcp list
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`-s user` makes memi available in every project, which is what you want because memory is shared across them. Drop it to limit memi to the current project. `MEMI_AGENT` is optional, see "Agent name" below.
|
|
49
|
+
|
|
50
|
+
### Other clients
|
|
51
|
+
|
|
52
|
+
Any client that can launch a stdio MCP server needs the same three things: a command, its arguments, and optional environment variables. In the common `mcpServers` JSON format:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"memi": {
|
|
58
|
+
"command": "node",
|
|
59
|
+
"args": ["/absolute/path/to/memi/dist/cli/index.js", "serve"],
|
|
60
|
+
"env": { "MEMI_AGENT": "my-agent" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use an absolute path. Restart the client after editing its config. memi has been tested with Claude Code. Other clients should work but have not been checked.
|
|
67
|
+
|
|
68
|
+
## Install the skills
|
|
69
|
+
|
|
70
|
+
Copy the folders in `skills/` (`memi-memory`, `memi-curate`, `memi-setup`) into your client's skills directory. For Claude Code that is `~/.claude/skills/`. Symlinks work too, so updates to the repo carry through.
|
|
71
|
+
|
|
72
|
+
## Agent name
|
|
73
|
+
|
|
74
|
+
Memories are tied to a project, which memi works out from the git repo folder name. In a folder with no git repo it falls back to the agent name, stored as `agent:<name>`. Without either, project memories cannot be saved.
|
|
75
|
+
|
|
76
|
+
Set the name with `memi config set agent <name>` or the `MEMI_AGENT` environment variable. `MEMI_PROJECT` forces a project name outright.
|
|
77
|
+
|
|
78
|
+
The agent name is also recorded on each memory so you can see who wrote it.
|
|
79
|
+
|
|
80
|
+
## Embedders
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
memi config # show what is in effect
|
|
84
|
+
memi config set embedder.provider openai # ollama or openai
|
|
85
|
+
memi config set embedder.model text-embedding-3-small
|
|
86
|
+
memi config set embedder.baseUrl http://localhost:11434
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Provider | Default model | Needs |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `ollama` | `nomic-embed-text` | Ollama running, the model pulled |
|
|
92
|
+
| `openai` | `text-embedding-3-small` | `OPENAI_API_KEY` in the environment that launches memi |
|
|
93
|
+
|
|
94
|
+
The API key is only ever read from the environment. memi never writes it to `config.json`. For an MCP client, put it in the server's `env` block (for Claude Code, add `-e OPENAI_API_KEY=...` to the `claude mcp add` command).
|
|
95
|
+
|
|
96
|
+
Environment variables override the config file: `MEMI_EMBED_PROVIDER`, `MEMI_EMBED_MODEL`, `MEMI_OLLAMA_URL`, `OPENAI_BASE_URL`.
|
|
97
|
+
|
|
98
|
+
### Switching embedders
|
|
99
|
+
|
|
100
|
+
> **Different models produce vectors with different dimensions, so existing vectors cannot be reused.**
|
|
101
|
+
|
|
102
|
+
When you change the provider or model, memi notices that the stored vectors came from something else. Semantic search turns off and keyword search carries on. Nothing is deleted. To finish the switch:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
memi reindex
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This drops the vector table, rebuilds it for the new model, and re-embeds every saved memory. **Your memories are kept.** It asks for confirmation (pass `--yes` in scripts). All the embedding happens before anything is dropped, so if the new embedder is unreachable, nothing changes. With OpenAI, reindexing sends every memory's text to OpenAI.
|
|
109
|
+
|
|
110
|
+
If you switch back to the model the vectors were built with, no reindex is needed.
|
|
111
|
+
|
|
112
|
+
## Where the data lives
|
|
113
|
+
|
|
114
|
+
- Database: `~/.memi/memi.db`. Config: `~/.memi/config.json`. Set `MEMI_HOME` to move both.
|
|
115
|
+
- SQLite runs in WAL mode, so you may see `memi.db-wal` and `memi.db-shm` next to the database while memi is running.
|
|
116
|
+
- To back up, copy the three files together while nothing is using the database, or use `sqlite3 ~/.memi/memi.db ".backup backup.db"`.
|
|
117
|
+
- Restoring the database brings back the memories and their vectors. If you restore it while using a different embedder, memi will ask for a reindex.
|
|
118
|
+
|
|
119
|
+
## Reading `memi doctor`
|
|
120
|
+
|
|
121
|
+
| Line | Meaning | Fix |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `fail Could not reach http://localhost:11434...` | Ollama is not running | Start Ollama |
|
|
124
|
+
| `fail ... model "x" not found, try pulling it first` | The model is not downloaded | `ollama pull x` |
|
|
125
|
+
| `fail OPENAI_API_KEY is not set` | Provider is `openai` with no key | Export the key where memi runs |
|
|
126
|
+
| `fail The embedder changed from ...` | Config no longer matches the stored vectors | `memi reindex`, or switch the config back |
|
|
127
|
+
| `warn N memories have no vector` | Saved while the embedder was down | `memi reindex --missing` (drops nothing) |
|
|
128
|
+
| `warn No project detected here` | No git repo, and no agent name | Set `MEMI_AGENT`, or run inside a git repo |
|
|
129
|
+
| `ok No vectors yet` | Nothing saved yet | Nothing. The index is created on the first save |
|
|
130
|
+
|
|
131
|
+
## The local UI
|
|
132
|
+
|
|
133
|
+
`memi ui` starts a memory manager at `http://localhost:4747`. Use it to browse, search, edit, pin, and delete memories. Add `--open` to open your browser, or `--port <n>` to change the port (or `memi config set ui.port <n>`). Press Ctrl+C to stop it.
|
|
134
|
+
|
|
135
|
+
It only listens on this machine and ignores requests that don't come from `localhost`. If it says the port is already in use, something else is on 4747 (often another `memi ui`), so pick another port.
|
|
136
|
+
|
|
137
|
+
If the UI shows a banner about the embedder, it is the same message as `memi doctor`. The UI doesn't run `memi reindex` for you, but the banner has the command with a copy button.
|
|
138
|
+
|
|
139
|
+
## If the MCP client shows no memi tools
|
|
140
|
+
|
|
141
|
+
1. Run `memi doctor` outside the client to rule out memi itself.
|
|
142
|
+
2. Check the path in the client config is absolute and that `npm run build` has been run.
|
|
143
|
+
3. Make sure the environment the client launches in can see `node`.
|
|
144
|
+
4. Restart the client.
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/* Buttons */
|
|
2
|
+
.btn {
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
gap: 8px;
|
|
7
|
+
min-height: var(--control);
|
|
8
|
+
padding: 0 14px;
|
|
9
|
+
border: 1px solid var(--border-strong);
|
|
10
|
+
border-radius: var(--radius);
|
|
11
|
+
background: transparent;
|
|
12
|
+
font-weight: 500;
|
|
13
|
+
white-space: nowrap;
|
|
14
|
+
transition: background-color 120ms var(--ease), border-color 120ms var(--ease), opacity 120ms var(--ease);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.btn:hover {
|
|
18
|
+
background: var(--surface-2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.btn:active {
|
|
22
|
+
background: var(--border);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.btn:disabled {
|
|
26
|
+
opacity: 0.5;
|
|
27
|
+
cursor: not-allowed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.btn-primary {
|
|
31
|
+
background: var(--text);
|
|
32
|
+
border-color: var(--text);
|
|
33
|
+
color: var(--bg);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.btn-primary:hover {
|
|
37
|
+
background: oklch(0.88 0.004 265);
|
|
38
|
+
border-color: oklch(0.88 0.004 265);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.btn-primary:active {
|
|
42
|
+
background: oklch(0.8 0.005 265);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.btn-danger {
|
|
46
|
+
border-color: var(--danger);
|
|
47
|
+
color: var(--danger);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.btn-danger:hover {
|
|
51
|
+
background: var(--danger-surface);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.btn-danger.solid {
|
|
55
|
+
background: var(--danger);
|
|
56
|
+
color: var(--bg);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.btn-danger.solid:hover {
|
|
60
|
+
background: oklch(0.76 0.16 25);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.btn-icon {
|
|
64
|
+
width: var(--control);
|
|
65
|
+
padding: 0;
|
|
66
|
+
border-color: transparent;
|
|
67
|
+
color: var(--text-dim);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.btn-icon:hover {
|
|
71
|
+
color: var(--text);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.btn-icon[aria-pressed="true"] {
|
|
75
|
+
color: var(--accent);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.btn-icon.danger:hover {
|
|
79
|
+
color: var(--danger);
|
|
80
|
+
background: var(--danger-surface);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.btn.loading {
|
|
84
|
+
opacity: 0.7;
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Inputs */
|
|
89
|
+
.input,
|
|
90
|
+
.select,
|
|
91
|
+
.textarea {
|
|
92
|
+
width: 100%;
|
|
93
|
+
min-height: var(--control);
|
|
94
|
+
padding: 0 12px;
|
|
95
|
+
border: 1px solid var(--border-strong);
|
|
96
|
+
border-radius: var(--radius);
|
|
97
|
+
background: var(--bg);
|
|
98
|
+
transition: border-color 120ms var(--ease);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.input:hover,
|
|
102
|
+
.select:hover,
|
|
103
|
+
.textarea:hover {
|
|
104
|
+
border-color: var(--text-faint);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.input::placeholder,
|
|
108
|
+
.textarea::placeholder {
|
|
109
|
+
color: var(--text-faint);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.textarea {
|
|
113
|
+
padding: 10px 12px;
|
|
114
|
+
line-height: 1.5;
|
|
115
|
+
resize: vertical;
|
|
116
|
+
min-height: 140px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.select {
|
|
120
|
+
width: auto;
|
|
121
|
+
padding-right: 8px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.input[aria-invalid="true"],
|
|
125
|
+
.textarea[aria-invalid="true"] {
|
|
126
|
+
border-color: var(--danger);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.search {
|
|
130
|
+
position: relative;
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.search > svg {
|
|
136
|
+
position: absolute;
|
|
137
|
+
left: 12px;
|
|
138
|
+
color: var(--text-faint);
|
|
139
|
+
pointer-events: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.search .input {
|
|
143
|
+
padding-left: 38px;
|
|
144
|
+
padding-right: var(--control);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* The page has its own clear button, so hide the browser's. */
|
|
148
|
+
.search .input::-webkit-search-cancel-button {
|
|
149
|
+
appearance: none;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.search .clear {
|
|
153
|
+
position: absolute;
|
|
154
|
+
right: 2px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Segmented control, used for search mode and importance */
|
|
158
|
+
.segmented {
|
|
159
|
+
display: inline-flex;
|
|
160
|
+
padding: 2px;
|
|
161
|
+
border: 1px solid var(--border-strong);
|
|
162
|
+
border-radius: var(--radius);
|
|
163
|
+
background: var(--bg);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.segmented > * {
|
|
167
|
+
flex: 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.segmented button,
|
|
171
|
+
.segmented label {
|
|
172
|
+
display: inline-flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
justify-content: center;
|
|
175
|
+
min-height: calc(var(--control) - 6px);
|
|
176
|
+
min-width: 36px;
|
|
177
|
+
padding: 0 10px;
|
|
178
|
+
border: 0;
|
|
179
|
+
border-radius: var(--radius-sm);
|
|
180
|
+
background: transparent;
|
|
181
|
+
color: var(--text-dim);
|
|
182
|
+
font-size: 13px;
|
|
183
|
+
font-weight: 500;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
white-space: nowrap;
|
|
186
|
+
transition: background-color 120ms var(--ease), color 120ms var(--ease);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.segmented button:hover,
|
|
190
|
+
.segmented label:hover {
|
|
191
|
+
color: var(--text);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.segmented button[aria-pressed="true"],
|
|
195
|
+
.segmented label:has(input:checked) {
|
|
196
|
+
background: var(--surface-2);
|
|
197
|
+
color: var(--text);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.segmented input {
|
|
201
|
+
position: absolute;
|
|
202
|
+
opacity: 0;
|
|
203
|
+
pointer-events: none;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.segmented label:has(input:focus-visible) {
|
|
207
|
+
outline: 2px solid var(--accent);
|
|
208
|
+
outline-offset: 1px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Sidebar navigation */
|
|
212
|
+
.nav-item {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
gap: 8px;
|
|
216
|
+
width: 100%;
|
|
217
|
+
min-height: var(--control);
|
|
218
|
+
padding: 0 8px;
|
|
219
|
+
border: 0;
|
|
220
|
+
border-radius: var(--radius-sm);
|
|
221
|
+
background: transparent;
|
|
222
|
+
color: var(--text-dim);
|
|
223
|
+
text-align: left;
|
|
224
|
+
transition: background-color 120ms var(--ease), color 120ms var(--ease);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.nav-item:hover {
|
|
228
|
+
background: var(--surface-2);
|
|
229
|
+
color: var(--text);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.nav-item[aria-current="true"] {
|
|
233
|
+
background: var(--surface-2);
|
|
234
|
+
color: var(--text);
|
|
235
|
+
font-weight: 500;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.nav-item .label {
|
|
239
|
+
flex: 1;
|
|
240
|
+
min-width: 0;
|
|
241
|
+
overflow: hidden;
|
|
242
|
+
text-overflow: ellipsis;
|
|
243
|
+
white-space: nowrap;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.nav-item .count {
|
|
247
|
+
color: var(--text-faint);
|
|
248
|
+
font: 400 12px/1 var(--font-mono);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.nav-item .here {
|
|
252
|
+
padding: 2px 6px;
|
|
253
|
+
border: 1px solid var(--border-strong);
|
|
254
|
+
border-radius: 999px;
|
|
255
|
+
color: var(--text-dim);
|
|
256
|
+
font-size: 11px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.check-row {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: 10px;
|
|
263
|
+
min-height: var(--control);
|
|
264
|
+
padding: 0 8px;
|
|
265
|
+
color: var(--text-dim);
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.check-row input {
|
|
270
|
+
width: 18px;
|
|
271
|
+
height: 18px;
|
|
272
|
+
margin: 0;
|
|
273
|
+
accent-color: var(--accent);
|
|
274
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/* Dialogs: the editor is a side sheet on desktop and full screen on phones. */
|
|
2
|
+
dialog {
|
|
3
|
+
padding: 0;
|
|
4
|
+
border: 1px solid var(--border-strong);
|
|
5
|
+
background: var(--surface);
|
|
6
|
+
color: var(--text);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
dialog::backdrop {
|
|
10
|
+
background: oklch(0.1 0.005 265 / 0.6);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
dialog.sheet {
|
|
14
|
+
width: min(560px, 100%);
|
|
15
|
+
height: 100dvh;
|
|
16
|
+
max-width: none;
|
|
17
|
+
max-height: none;
|
|
18
|
+
margin: 0 0 0 auto;
|
|
19
|
+
border-width: 0 0 0 1px;
|
|
20
|
+
border-radius: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
dialog.sheet[open] {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
animation: sheet-in 180ms var(--ease);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
dialog.modal {
|
|
30
|
+
width: min(420px, calc(100% - 2 * var(--gutter)));
|
|
31
|
+
margin: auto;
|
|
32
|
+
border-radius: var(--radius);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dialog.modal[open] {
|
|
36
|
+
animation: fade-in 140ms var(--ease);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@keyframes sheet-in {
|
|
40
|
+
from {
|
|
41
|
+
opacity: 0;
|
|
42
|
+
transform: translateX(24px);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@keyframes fade-in {
|
|
47
|
+
from {
|
|
48
|
+
opacity: 0;
|
|
49
|
+
transform: translateY(6px);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dialog-form {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
min-height: 0;
|
|
57
|
+
flex: 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dialog-head {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: space-between;
|
|
64
|
+
gap: 8px;
|
|
65
|
+
padding: 12px var(--gutter);
|
|
66
|
+
border-bottom: 1px solid var(--border);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.dialog-head h2 {
|
|
70
|
+
margin: 0;
|
|
71
|
+
font-size: 16px;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.dialog-body {
|
|
76
|
+
flex: 1;
|
|
77
|
+
min-height: 0;
|
|
78
|
+
overflow-y: auto;
|
|
79
|
+
display: grid;
|
|
80
|
+
align-content: start;
|
|
81
|
+
gap: 18px;
|
|
82
|
+
padding: var(--gutter);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.dialog-foot {
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-wrap: wrap;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 8px;
|
|
90
|
+
padding: 12px var(--gutter);
|
|
91
|
+
border-top: 1px solid var(--border);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.dialog-foot .spacer {
|
|
95
|
+
flex: 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.modal .dialog-body {
|
|
99
|
+
gap: 8px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.modal p {
|
|
103
|
+
margin: 0;
|
|
104
|
+
color: var(--text-dim);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.field {
|
|
108
|
+
display: grid;
|
|
109
|
+
gap: 6px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.field > label,
|
|
113
|
+
.field > .label {
|
|
114
|
+
color: var(--text-dim);
|
|
115
|
+
font-size: 13px;
|
|
116
|
+
font-weight: 500;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.field-row {
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-wrap: wrap;
|
|
122
|
+
gap: 8px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.field-row > .input {
|
|
126
|
+
flex: 1 1 180px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.form-error {
|
|
130
|
+
margin: 0;
|
|
131
|
+
color: var(--danger);
|
|
132
|
+
font-size: 14px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.fine {
|
|
136
|
+
color: var(--text-faint);
|
|
137
|
+
font: 400 12px/1.5 var(--font-mono);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@media (max-width: 899px) {
|
|
141
|
+
dialog.sheet {
|
|
142
|
+
width: 100%;
|
|
143
|
+
border-width: 0;
|
|
144
|
+
}
|
|
145
|
+
}
|