anentrypoint-design 0.0.322 → 0.0.324

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.
Files changed (45) hide show
  1. package/README.md +22 -2
  2. package/app-shell.css +24 -4782
  3. package/colors_and_type.css +43 -0
  4. package/dist/247420.css +74 -3
  5. package/dist/247420.js +16 -16
  6. package/package.json +14 -8
  7. package/src/bootstrap.js +10 -1
  8. package/src/components/chat-message-parts.js +355 -0
  9. package/src/components/chat.js +23 -320
  10. package/src/components/data-density.js +15 -10
  11. package/src/components/files-modals.js +14 -2
  12. package/src/components/files.js +5 -1
  13. package/src/components/freddie/helpers.js +46 -8
  14. package/src/css/app-shell/base.css +157 -0
  15. package/src/css/app-shell/catalog-theme.css +144 -0
  16. package/src/css/app-shell/chat-basic.css +12 -0
  17. package/src/css/app-shell/chat-polish.css +459 -0
  18. package/src/css/app-shell/data-density.css +150 -0
  19. package/src/css/app-shell/files.css +709 -0
  20. package/src/css/app-shell/hero-content.css +407 -0
  21. package/src/css/app-shell/kits-appended.css +361 -0
  22. package/src/css/app-shell/loading-alerts.css +120 -0
  23. package/src/css/app-shell/panel-row.css +177 -0
  24. package/src/css/app-shell/primitives.css +197 -0
  25. package/src/css/app-shell/responsive.css +266 -0
  26. package/src/css/app-shell/responsive2-workspace.css +465 -0
  27. package/src/css/app-shell/row-print.css +75 -0
  28. package/src/css/app-shell/sidebar-misc.css +79 -0
  29. package/src/css/app-shell/states-interactions.css +696 -0
  30. package/src/css/app-shell/topbar.css +319 -0
  31. package/src/deck-stage.js +6 -4
  32. package/src/highlight.js +20 -4
  33. package/src/index.js +15 -5
  34. package/src/kits/os/freddie/pages-chat.js +1 -1
  35. package/src/kits/os/freddie/pages-core.js +1 -1
  36. package/src/kits/os/freddie/pages-os.js +1 -1
  37. package/src/kits/os/freddie/pages-tools.js +1 -1
  38. package/src/kits/os/wm.js +38 -0
  39. package/src/kits/slides/deck-stage-overlay.js +68 -0
  40. package/src/kits/slides/deck-stage-state.js +81 -0
  41. package/src/kits/slides/deck-stage-style.js +117 -0
  42. package/src/kits/slides/deck-stage.js +167 -0
  43. package/src/kits/slides/index.js +5 -0
  44. package/src/markdown.js +89 -11
  45. package/src/kits/os/freddie/helpers.js +0 -33
@@ -0,0 +1,709 @@
1
+ /* ============================================================
2
+ File surface — responsive grid + row layouts
3
+ ============================================================ */
4
+
5
+ /* Default file grid — a vertical list of rows. Each row lays out its own
6
+ columns internally; the container only stacks them. Card mode (multi-column)
7
+ is opt-in via [data-columns]. */
8
+ .ds-file-grid {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 4px;
12
+ }
13
+ .ds-file-grid:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); border-radius: var(--r-2); }
14
+
15
+ /* Listing wrapper holds the optional filter + sort header above the grid. */
16
+ .ds-file-listing { display: flex; flex-direction: column; gap: var(--space-2); min-height: 0; }
17
+ /* The quick filter is a compact control, not a full-width form row - a giant
18
+ input above the grid reads as a search page and costs fold height. */
19
+ /* The filter is now an inline item in the single .ds-file-controls toolbar row
20
+ (the standalone right-aligned .ds-file-filter strip was removed). */
21
+ .ds-file-filter-input {
22
+ width: min(280px, 100%); padding: 6px 12px; font-size: var(--fs-sm);
23
+ background: var(--bg); color: var(--fg);
24
+ border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
25
+ }
26
+ .ds-file-controls .ds-file-filter-wrap { flex: 0 1 220px; display: contents; }
27
+ .ds-file-controls .ds-file-filter-wrap .ds-file-filter-input { flex: 0 1 220px; }
28
+ .ds-file-filter-input:focus-visible { outline: none; box-shadow: var(--focus-ring-inset); }
29
+ .ds-file-sort { display: flex; gap: var(--space-2); padding: 0 var(--space-2) var(--space-1); }
30
+ .ds-file-sort-btn {
31
+ font-size: var(--fs-tiny); font-family: var(--ff-mono); color: var(--fg-3);
32
+ background: none; border: none; cursor: pointer; padding: 2px 4px; border-radius: var(--r-1);
33
+ }
34
+ .ds-file-sort-btn:hover { color: var(--fg-2); background: var(--bg-2); }
35
+ .ds-file-sort-btn.active { color: var(--accent-ink); }
36
+ .ds-file-sort-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
37
+
38
+ /* Skeleton shimmer rows shown while a directory loads. */
39
+ .ds-file-row-skeleton { cursor: default; pointer-events: none; }
40
+ /* In-place refresh: dim the populated grid instead of swapping it for shimmer
41
+ rows (the rows stay; the round-trip reads as a settle, not a reload). */
42
+ .ds-file-grid.is-refreshing { opacity: .6; }
43
+ @media (prefers-reduced-motion: no-preference) {
44
+ .ds-file-grid { transition: opacity .15s var(--ease); }
45
+ }
46
+ /* EventList loading skeleton (shares the .ds-skel shimmer base above). */
47
+ .ds-event-row-skeleton { display: flex; align-items: center; gap: var(--space-3); padding: 12px 16px; pointer-events: none; }
48
+ .ds-skel-rank { height: 12px; width: 2.5ch; }
49
+ .ds-event-state { padding: var(--space-2) 16px; }
50
+ .ds-skel { display: inline-block; border-radius: var(--r-1); background: var(--bg-2); position: relative; overflow: hidden; }
51
+ .ds-skel::after {
52
+ content: ''; position: absolute; inset: 0;
53
+ background: linear-gradient(90deg, transparent, var(--bg-3), transparent);
54
+ transform: translateX(-100%); animation: ds-skel-shimmer 1.2s infinite;
55
+ }
56
+ .ds-skel-icon { width: 22px; height: 22px; }
57
+ .ds-skel-title { height: 12px; width: 60%; }
58
+ .ds-skel-meta { height: 10px; width: 64px; margin-left: auto; }
59
+ @keyframes ds-skel-shimmer { 100% { transform: translateX(100%); } }
60
+ @media (prefers-reduced-motion: reduce) { .ds-skel::after { animation: none; } }
61
+
62
+ /* File row — linear layout (checkbox? · open-button[code · icon · title · meta] · actions).
63
+ The row is a flex strip; the open <button> is the stretching member and lays
64
+ out the entry's own columns. It needs a full reset (UA button chrome off)
65
+ because FileRow wraps the row content in a real button for native keyboard
66
+ semantics. */
67
+ .ds-file-row {
68
+ display: flex; align-items: center; gap: 10px;
69
+ padding: 11px 16px; background: var(--bg);
70
+ border: var(--bw-hair) solid transparent;
71
+ border-radius: var(--r-2); color: var(--fg);
72
+ cursor: pointer;
73
+ transition: background var(--dur-snap) var(--ease),
74
+ border-color var(--dur-snap) var(--ease),
75
+ transform var(--dur-snap) var(--ease);
76
+ }
77
+ .ds-file-open {
78
+ display: flex; align-items: center; gap: 14px;
79
+ flex: 1 1 auto; min-width: 0;
80
+ background: none; border: 0; padding: 0; margin: 0;
81
+ color: inherit; font: inherit; text-align: left; cursor: pointer;
82
+ }
83
+ .ds-file-open[disabled] { cursor: default; }
84
+ .ds-file-open:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); border-radius: var(--r-1); }
85
+ .ds-file-row:hover {
86
+ background: var(--bg-2);
87
+ border-color: var(--rule);
88
+ }
89
+ .ds-file-row:active { transform: translateY(1px); }
90
+ .ds-file-row.active { background: var(--accent-tint); border-color: var(--accent); }
91
+ .ds-file-row[data-file-type="dir"] { border-left-width: 3px; border-left-color: var(--accent); }
92
+ .ds-file-row[data-file-type="image"],
93
+ .ds-file-row[data-file-type="video"],
94
+ .ds-file-row[data-file-type="audio"],
95
+ .ds-file-row[data-file-type="code"],
96
+ .ds-file-row[data-file-type="text"],
97
+ .ds-file-row[data-file-type="archive"],
98
+ .ds-file-row[data-file-type="document"],
99
+ .ds-file-row[data-file-type="symlink"],
100
+ .ds-file-row[data-file-type="other"] { border-left-width: 3px; border-left-color: var(--rule); }
101
+
102
+ /* Row internals — these are the classes FileRow actually renders. */
103
+ .ds-file-row .title {
104
+ font-weight: 500; font-size: var(--fs-sm);
105
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
106
+ min-width: 0; flex: 1 1 auto;
107
+ }
108
+ .ds-file-row .code {
109
+ font-family: var(--ff-mono); font-size: var(--fs-micro);
110
+ color: var(--fg-3);
111
+ }
112
+ .ds-file-row .ds-file-meta {
113
+ font-family: var(--ff-mono); font-size: var(--fs-xs);
114
+ color: var(--fg-3); white-space: nowrap;
115
+ /* Never shrink below content: a flex sibling with white-space:nowrap and
116
+ the default flex-shrink:1 silently overflows its own box (no ellipsis)
117
+ when the row runs out of width, painting the date/size over the title
118
+ and action icons at mobile widths. Fixed-size the meta column instead
119
+ and let .title (flex:1, min-width:0, ellipsis) absorb the squeeze. */
120
+ flex-shrink: 0;
121
+ }
122
+
123
+ /* File icon — glyph chip, tinted per type. */
124
+ .ds-file-icon {
125
+ display: inline-flex; align-items: center; justify-content: center;
126
+ width: 26px; height: 26px; font-size: 15px; line-height: 1;
127
+ color: var(--fg-2);
128
+ }
129
+ .ds-file-row[data-file-type="dir"] .ds-file-icon { color: var(--accent-ink); }
130
+ .ds-file-row[data-file-type="image"] .ds-file-icon { color: var(--mascot); }
131
+ .ds-file-row[data-file-type="video"] .ds-file-icon { color: var(--purple-2); }
132
+ .ds-file-row[data-file-type="audio"] .ds-file-icon { color: var(--sky); }
133
+ .ds-file-row[data-file-type="code"] .ds-file-icon { color: var(--green-2); }
134
+ .ds-file-row[data-file-type="archive"] .ds-file-icon { color: var(--flame); }
135
+ .ds-file-row[data-file-type="document"] .ds-file-icon { color: var(--amber, var(--sun)); }
136
+
137
+ /* Row actions — a faint baseline keeps them discoverable at rest (not fully
138
+ hidden, which left mouse/trackpad users unable to find rename/move/delete
139
+ without accidentally hovering a row), full opacity on hover/focus. */
140
+ .ds-file-actions {
141
+ display: inline-flex; gap: 2px; align-items: center;
142
+ opacity: 0.35; transition: opacity var(--dur-base) var(--ease);
143
+ }
144
+ .ds-file-row:hover .ds-file-actions,
145
+ .ds-file-row:focus-within .ds-file-actions { opacity: 1; }
146
+ .ds-file-act {
147
+ display: inline-flex; align-items: center; justify-content: center;
148
+ width: 28px; height: 28px; padding: 0;
149
+ background: transparent; border: 0; border-radius: var(--r-1);
150
+ color: var(--fg-3); font-size: 14px; cursor: pointer;
151
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
152
+ }
153
+ .ds-file-act:hover { background: var(--bg-3); color: var(--fg); }
154
+ .ds-file-act-warn:hover { background: color-mix(in oklab, var(--warn) 18%, var(--bg)); color: var(--warn); }
155
+
156
+ /* Multi-select — per-row checkbox, header tri-state, bulk action strip. */
157
+ /* CSS-drawn multi-select checkbox: a bordered box that fills with the accent +
158
+ a border-drawn tick when its parent button is .is-marked or aria-checked.
159
+ Shared by FileRow/FileCell (.is-marked) and the file/session select-all +
160
+ SessionCard select (aria-checked true/mixed) since the bundle concatenates
161
+ all CSS. Replaces the old [x]/[ ] bracket text - no glyphs, AT keeps the
162
+ role=checkbox/aria-checked name+state.
163
+ See also .ds-check in editor-primitives.css — a separate native-input
164
+ checkbox family for form/settings contexts; intentionally distinct
165
+ rendering (native vs CSS-drawn), do not let one drift toward the other
166
+ by accident. */
167
+ .ds-check-box {
168
+ width: 15px; height: 15px; box-sizing: border-box; flex: 0 0 auto; position: relative;
169
+ border: 1.5px solid var(--fg-3); border-radius: var(--r-0);
170
+ transition: background var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease);
171
+ }
172
+ .is-marked > .ds-check-box,
173
+ [aria-checked="true"] > .ds-check-box { background: var(--fg); border-color: var(--fg); }
174
+ .is-marked > .ds-check-box::after,
175
+ [aria-checked="true"] > .ds-check-box::after {
176
+ content: ""; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px;
177
+ border: solid var(--bg); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg);
178
+ }
179
+ [aria-checked="mixed"] > .ds-check-box { background: var(--fg); border-color: var(--fg); }
180
+ [aria-checked="mixed"] > .ds-check-box::after {
181
+ content: ""; position: absolute; left: 3px; right: 3px; top: 6px; height: 2px; background: var(--bg); border-radius: var(--r-hair);
182
+ }
183
+
184
+ .ds-file-check {
185
+ display: inline-flex; align-items: center; justify-content: center;
186
+ min-width: 28px; height: 28px; padding: 0 4px;
187
+ background: transparent; border: 0; border-radius: var(--r-1);
188
+ color: var(--fg-3);
189
+ cursor: pointer;
190
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
191
+ }
192
+ .ds-file-check:hover { background: var(--bg-3); color: var(--fg); }
193
+ .ds-file-check.is-marked { color: var(--accent-ink); }
194
+ .ds-file-check:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
195
+ .ds-file-check[disabled] { opacity: 0.4; cursor: default; }
196
+ .ds-file-row.is-marked { background: var(--accent-tint); border-color: var(--accent); }
197
+ .ds-file-selectall {
198
+ display: inline-flex; align-items: center; gap: var(--space-1);
199
+ font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-2);
200
+ cursor: pointer; background: none; border: none;
201
+ padding: var(--space-1); border-radius: var(--r-1);
202
+ }
203
+ .ds-file-selectall:hover { background: var(--bg-2); color: var(--fg); }
204
+ .ds-file-selectall:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
205
+ .ds-file-controls { display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-2) var(--space-1); flex-wrap: wrap; }
206
+ .ds-file-controls .spread { flex: 1 1 auto; }
207
+ .ds-file-controls .ds-file-sort { padding: 0; }
208
+ .ds-bulkbar {
209
+ display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
210
+ padding: var(--space-1) var(--space-2);
211
+ background: var(--bg-2); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
212
+ }
213
+ .ds-bulkbar-count { font-size: var(--fs-sm); font-weight: 600; color: var(--fg-2); }
214
+ /* BulkBar is a toolbar (it even declares role=toolbar): compact rect buttons,
215
+ quiet destructive outline - the app arm-confirms destructive bulk actions
216
+ via ConfirmDialog, so the strip never needs a loud CTA. */
217
+ .ds-bulkbar .btn, .ds-bulkbar .btn-primary {
218
+ padding: 5px 12px; min-height: 32px; border-radius: var(--r-1); font-weight: 500;
219
+ }
220
+ .ds-bulkbar .btn { background: transparent; border: var(--bw-hair) solid var(--rule); color: var(--fg-2); }
221
+ .ds-bulkbar .btn:hover { background: var(--bg-2); color: var(--fg); }
222
+ .ds-bulkbar .btn-primary.danger { background: transparent; color: var(--warn); border: var(--bw-hair) solid var(--warn); }
223
+ .ds-bulkbar .btn-primary.danger:hover { background: color-mix(in oklab, var(--warn) 12%, transparent); color: var(--warn); }
224
+ @media (pointer: coarse) {
225
+ .ds-bulkbar .btn, .ds-bulkbar .btn-primary { min-height: 44px; }
226
+ }
227
+
228
+ /* Density picker — list / compact / thumbnails. */
229
+ .ds-density {
230
+ display: inline-flex; gap: 2px; padding: 2px;
231
+ background: var(--bg-2); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
232
+ }
233
+ .ds-density-btn {
234
+ display: inline-flex; align-items: center; justify-content: center;
235
+ width: 28px; height: 28px; color: var(--fg-2);
236
+ background: none; border: none; cursor: pointer;
237
+ padding: 0; border-radius: calc(var(--r-1) - 2px);
238
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
239
+ }
240
+ .ds-density-btn:hover { color: var(--fg); }
241
+ .ds-density-btn.active { background: var(--accent-tint); color: var(--fg); }
242
+ .ds-density-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
243
+
244
+ /* Compact density — tighter rows for long listings. */
245
+ .ds-file-grid[data-density="compact"] { gap: 2px; }
246
+ .ds-file-grid[data-density="compact"] .ds-file-row { padding: 5px 12px; gap: 10px; }
247
+ .ds-file-grid[data-density="compact"] .ds-file-row .title { font-size: var(--fs-xs); }
248
+ .ds-file-grid[data-density="compact"] .ds-file-row .ds-file-meta { font-size: var(--fs-micro); }
249
+ .ds-file-grid[data-density="compact"] .ds-file-icon { width: 20px; height: 20px; font-size: 13px; }
250
+
251
+ /* Thumbnail density — tile grid; image entries carry a real lazy thumbnail. */
252
+ .ds-file-grid-thumb {
253
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
254
+ gap: var(--space-3); align-items: start;
255
+ }
256
+ .ds-file-cell {
257
+ position: relative;
258
+ background: var(--bg); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
259
+ overflow: hidden;
260
+ transition: border-color var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease);
261
+ }
262
+ .ds-file-cell:hover { border-color: var(--fg-3); }
263
+ .ds-file-cell.active { border-color: var(--accent); background: var(--accent-tint); }
264
+ .ds-file-cell.is-marked { border-color: var(--accent); background: var(--accent-tint); }
265
+ .ds-file-cell.is-locked { opacity: 0.6; }
266
+ .ds-file-row.is-locked { opacity: 0.6; }
267
+ .ds-file-row.is-restricted .title { color: var(--fg-2); }
268
+ /* A fourth, deliberately bordered/mono tag variant for file-permission tags
269
+ specifically (distinct from .ds-badge for numeric counts and .chip for
270
+ status/label words) - is-noaccess reuses the shared --flame tone token
271
+ so 'no access' reads the same red as chip.tone-error/miss elsewhere. */
272
+ .ds-file-perm-tag { font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--fg-3); padding: 1px 6px; border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1); white-space: nowrap; }
273
+ .ds-file-perm-tag.is-noaccess { color: var(--flame); border-color: var(--flame); }
274
+ .ds-file-cell-open {
275
+ display: flex; flex-direction: column; align-items: stretch; gap: var(--space-1);
276
+ width: 100%; padding: var(--space-1); margin: 0;
277
+ background: none; border: none; cursor: pointer; color: var(--fg);
278
+ }
279
+ .ds-file-cell-open[disabled] { cursor: default; }
280
+ .ds-file-cell-open:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: -2px; border-radius: var(--r-2); }
281
+ .ds-file-cell-media {
282
+ display: flex; align-items: center; justify-content: center;
283
+ width: 100%; aspect-ratio: 4 / 3;
284
+ background: var(--bg-2); border-radius: var(--r-1); overflow: hidden;
285
+ }
286
+ .ds-file-cell-media .ds-file-icon { width: 34px; height: 34px; font-size: 24px; }
287
+ .ds-file-cell-thumb { width: 100%; height: 100%; object-fit: cover; }
288
+ /* Per-type icon tints on thumb tiles so the grid keeps the colour-coding the
289
+ list rows already carry (mirrors .ds-file-row[data-file-type] .ds-file-icon). */
290
+ .ds-file-cell[data-file-type="dir"] .ds-file-icon { color: var(--accent-ink); }
291
+ .ds-file-cell[data-file-type="image"] .ds-file-icon { color: var(--mascot); }
292
+ .ds-file-cell[data-file-type="video"] .ds-file-icon { color: var(--purple-2); }
293
+ .ds-file-cell[data-file-type="audio"] .ds-file-icon { color: var(--sky); }
294
+ .ds-file-cell[data-file-type="code"] .ds-file-icon { color: var(--green-2); }
295
+ .ds-file-cell[data-file-type="archive"] .ds-file-icon { color: var(--flame); }
296
+ .ds-file-cell[data-file-type="document"] .ds-file-icon { color: var(--amber); }
297
+ .ds-file-cell[data-file-type="symlink"] .ds-file-icon { color: var(--purple); }
298
+ .ds-file-cell-name {
299
+ font-size: var(--fs-tiny); text-align: left;
300
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
301
+ }
302
+ .ds-file-cell-meta { font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--fg-3); text-align: left; }
303
+ .ds-file-cell-check {
304
+ position: absolute; top: 4px; left: 4px; z-index: 1;
305
+ background: color-mix(in oklab, var(--bg) 78%, transparent);
306
+ }
307
+
308
+ /* ============================================================
309
+ File browser surface — stage, toolbar, breadcrumb, dropzone,
310
+ upload progress, empty state, modals, preview. Authored for
311
+ fsbrowse but lives in the shared kit under .ds-247420 scope.
312
+ ============================================================ */
313
+
314
+ /* Stage — the scrollable content column. */
315
+ .ds-file-stage {
316
+ display: flex; flex-direction: column; gap: var(--space-3);
317
+ width: 100%; max-width: var(--measure-wide); margin: 0 auto;
318
+ padding: var(--space-2) var(--space-3) var(--space-4);
319
+ }
320
+ /* When the stage is the app-main child, the shell already supplies top
321
+ padding — collapse the stage's own so content isn't pushed down. */
322
+ .app-main > div > .ds-file-stage,
323
+ .app-main .ds-file-stage { padding-top: 0; }
324
+
325
+ /* Full-width files stack: the stage's space-3 vertical beat without its
326
+ --measure-wide cap/auto-margins, so the roots / toolbar / bulkbar / uploads / grid
327
+ bands keep one consistent rhythm instead of touching edge-to-edge. Carries
328
+ the same side/bottom padding as .ds-file-stage (top collapses the same way
329
+ when mounted directly under .app-main, which already supplies top padding)
330
+ so switching a consumer from the capped stage to this full-width stack is a
331
+ pure width change, not a spacing regression. */
332
+ .ds-files-stack {
333
+ display: flex; flex-direction: column; gap: var(--space-3); min-height: 0;
334
+ padding: 0 var(--space-3) var(--space-4);
335
+ }
336
+ .app-main > div > .ds-files-stack,
337
+ .app-main .ds-files-stack { padding-top: 0; }
338
+
339
+ /* Breadcrumb path */
340
+ .ds-crumb-path {
341
+ display: flex; align-items: center; flex-wrap: wrap; gap: 2px;
342
+ font-size: var(--fs-sm);
343
+ }
344
+ .ds-crumb-seg {
345
+ background: transparent; border: 0; cursor: pointer;
346
+ padding: 4px 8px; border-radius: var(--r-1);
347
+ color: var(--fg-3); font-family: inherit; font-size: var(--fs-sm);
348
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
349
+ /* A single long unbroken segment (hash-named dir) must never force the
350
+ whole path past the viewport. */
351
+ max-width: min(40vw, 220px);
352
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
353
+ }
354
+ .ds-crumb-seg:hover { background: var(--bg-2); color: var(--fg); }
355
+ .ds-crumb-seg.leaf { color: var(--fg); font-weight: 600; }
356
+ .ds-crumb-sep { color: var(--fg-3); opacity: 0.6; user-select: none; }
357
+
358
+ /* Toolbar */
359
+ .ds-file-toolbar {
360
+ display: flex; align-items: center; justify-content: space-between;
361
+ gap: var(--space-2); flex-wrap: wrap;
362
+ }
363
+ /* Dense page header: one row, small heading, single-line muted lede. The
364
+ working surfaces (files, live, history, settings) lead with content, not a
365
+ display H1 + paragraph that costs the first 150px of every fold. */
366
+ .ds-page-header-dense { margin: 0 0 var(--space-3); }
367
+ .ds-page-header-dense-row { display: flex; align-items: baseline; gap: var(--space-3); min-width: 0; }
368
+ .ds-page-header-dense-row h1 {
369
+ margin: 0; font-size: var(--fs-lg); line-height: 1.3; font-weight: 600;
370
+ letter-spacing: normal; flex: 0 0 auto;
371
+ }
372
+ .ds-page-header-dense-lede {
373
+ flex: 1 1 auto; min-width: 0;
374
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
375
+ font-size: var(--fs-sm); color: var(--fg-3);
376
+ }
377
+ .ds-page-header-dense .ds-page-header-right { flex: 0 0 auto; margin-left: auto; }
378
+ /* Below ~480px the single-line ellipsis truncates the majority of the lede
379
+ text with no way to recover it (no title attr, no expand) - let the row
380
+ wrap and the lede break onto its own line instead of clipping. */
381
+ @media (max-width: 480px) {
382
+ .ds-page-header-dense-row { flex-wrap: wrap; }
383
+ .ds-page-header-dense-lede {
384
+ flex-basis: 100%; overflow: visible; text-overflow: clip; white-space: normal;
385
+ }
386
+ }
387
+ .ds-file-toolbar-left,
388
+ .ds-file-toolbar-right {
389
+ display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
390
+ }
391
+ /* Toolbar-scoped buttons: quiet, compact, rectangular - a toolbar is chrome,
392
+ not a CTA row; the stadium pill stays for real page-level actions. */
393
+ .ds-file-toolbar .btn, .ds-file-toolbar .btn-ghost {
394
+ padding: 5px 12px; min-height: 32px;
395
+ border-radius: var(--r-1);
396
+ font-weight: 500;
397
+ background: transparent;
398
+ border: var(--bw-hair) solid var(--rule); color: var(--fg-2);
399
+ }
400
+ .ds-file-toolbar .btn:hover, .ds-file-toolbar .btn-ghost:hover { background: var(--bg-2); color: var(--fg); }
401
+ @media (pointer: coarse) {
402
+ .ds-file-toolbar .btn, .ds-file-toolbar .btn-ghost { min-height: 44px; }
403
+ }
404
+ .ds-meta-mono {
405
+ font-family: var(--ff-mono); font-size: var(--fs-xs);
406
+ color: var(--fg-3); letter-spacing: var(--tr-caps);
407
+ }
408
+
409
+ /* Error banner — dismissible toast row. */
410
+ .ds-error-banner {
411
+ padding: 10px 14px; border-radius: var(--r-2);
412
+ background: color-mix(in oklab, var(--warn) 14%, var(--bg));
413
+ border: var(--bw-hair) solid color-mix(in oklab, var(--warn) 40%, transparent);
414
+ color: var(--warn); font-size: var(--fs-sm); cursor: pointer;
415
+ }
416
+ .ds-error-banner:hover { background: color-mix(in oklab, var(--warn) 20%, var(--bg)); }
417
+
418
+ /* Dropzone */
419
+ .ds-dropzone {
420
+ border: var(--bw-rule) dashed var(--rule);
421
+ border-radius: var(--r-3); background: var(--bg);
422
+ transition: border-color var(--dur-base) var(--ease), background var(--dur-base) var(--ease);
423
+ }
424
+ .ds-dropzone-inner {
425
+ display: flex; align-items: center; justify-content: center;
426
+ gap: var(--space-2); flex-wrap: wrap;
427
+ padding: var(--space-4) var(--space-3); text-align: center;
428
+ }
429
+ .ds-dropzone-glyph { font-size: 22px; color: var(--fg-3); }
430
+ .ds-dropzone-label { font-size: var(--fs-sm); color: var(--fg-3); }
431
+ .ds-dropzone.dragover {
432
+ border-color: var(--accent);
433
+ background: var(--accent-tint);
434
+ }
435
+ @media (prefers-reduced-motion: no-preference) {
436
+ .ds-dropzone.dragover { animation: ds-dropzone-pulse 1.1s var(--ease) infinite; }
437
+ }
438
+ .ds-dropzone.dragover .ds-dropzone-glyph,
439
+ .ds-dropzone.dragover .ds-dropzone-label { color: var(--accent-ink); }
440
+ @keyframes ds-dropzone-pulse {
441
+ 0%, 100% { border-color: var(--accent); }
442
+ 50% { border-color: color-mix(in oklab, var(--accent) 45%, transparent); }
443
+ }
444
+ /* Wrapper form (has children): no permanent chrome - the content renders as if
445
+ the zone were not there, and the dashed affordance overlays it ONLY while a
446
+ drag is in flight. */
447
+ .ds-dropzone--wrap { position: relative; border: none; border-radius: 0; background: transparent; }
448
+ .ds-dropzone--wrap > .ds-dropzone-inner { display: none; }
449
+ .ds-dropzone--wrap.dragover > .ds-dropzone-inner {
450
+ display: flex; position: absolute; inset: 0; z-index: 5;
451
+ margin: 0; padding: var(--space-3);
452
+ align-items: center; justify-content: center;
453
+ background: color-mix(in srgb, var(--bg) 84%, transparent);
454
+ outline: var(--bw-rule) dashed var(--accent); outline-offset: -6px;
455
+ border-radius: var(--r-3);
456
+ }
457
+ .ds-dropzone--wrap.dragover { animation: none; }
458
+ /* Touch devices can never fire dragover — keep the pick-files affordance
459
+ visible as a compact static row so upload stays reachable in wrap mode. */
460
+ @media (hover: none) and (pointer: coarse) {
461
+ .ds-dropzone--wrap > .ds-dropzone-inner {
462
+ display: flex; position: static; padding: var(--space-2);
463
+ outline: none; background: transparent;
464
+ }
465
+ }
466
+
467
+ /* Upload progress */
468
+ .ds-upload-progress { display: flex; flex-direction: column; gap: 6px; }
469
+ .ds-upload-item {
470
+ display: grid; grid-template-columns: minmax(0, 1fr) 120px 44px;
471
+ gap: var(--space-2); align-items: center;
472
+ padding: 8px 12px; border-radius: var(--r-2);
473
+ background: var(--bg-2); font-size: var(--fs-xs);
474
+ }
475
+ .ds-upload-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
476
+ .ds-upload-bar {
477
+ height: 6px; border-radius: var(--r-pill);
478
+ background: var(--bg-3); overflow: hidden;
479
+ }
480
+ .ds-upload-fill {
481
+ display: block; height: 100%; width: 0;
482
+ background: var(--fg-3); border-radius: var(--r-pill);
483
+ transition: width var(--dur-base) var(--ease);
484
+ }
485
+ .ds-upload-fill.is-active { background: var(--accent); }
486
+ /* Indeterminate: percent is unknown mid-transfer (fetch gives no chunk-level
487
+ progress) — a sliding marquee bar reads as "working", not stuck at 0%. */
488
+ .ds-upload-bar.indeterminate .ds-upload-fill {
489
+ width: 40%; transition: none;
490
+ animation: ds-upload-marquee 1.1s ease-in-out infinite;
491
+ }
492
+ @keyframes ds-upload-marquee {
493
+ 0% { transform: translateX(-100%); }
494
+ 100% { transform: translateX(250%); }
495
+ }
496
+ @media (prefers-reduced-motion: reduce) {
497
+ .ds-upload-bar.indeterminate .ds-upload-fill { animation: none; width: 100%; opacity: 0.5; }
498
+ }
499
+ .ds-upload-pct {
500
+ font-family: var(--ff-mono); font-size: var(--fs-micro);
501
+ color: var(--fg-3); text-align: right;
502
+ }
503
+ .ds-upload-item.done .ds-upload-fill { background: var(--success); }
504
+ .ds-upload-item.done .ds-upload-pct { color: var(--green); }
505
+ .ds-upload-item.error .ds-upload-fill { background: var(--warn); }
506
+ .ds-upload-item.error .ds-upload-pct { color: var(--warn); }
507
+ /* Per-row upload recovery actions (replace / dismiss) — error rows are not dead ends. */
508
+ .ds-upload-item { grid-template-columns: minmax(0, 1fr) minmax(60px, 120px) 44px auto; }
509
+ @media (max-width: 480px) {
510
+ .ds-upload-item { grid-template-columns: minmax(0, 1fr) 44px; }
511
+ .ds-upload-bar { grid-column: 1 / -1; order: 3; }
512
+ .ds-upload-actions { grid-column: 1 / -1; justify-content: flex-end; }
513
+ }
514
+ .ds-upload-actions { display: inline-flex; gap: var(--space-1); justify-content: flex-end; }
515
+ .ds-upload-act {
516
+ padding: 2px 10px; min-height: 24px; cursor: pointer;
517
+ background: var(--bg); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
518
+ color: var(--fg-2); font-family: var(--ff-body); font-size: var(--fs-micro);
519
+ }
520
+ .ds-upload-act:hover { background: var(--bg-3); color: var(--fg); }
521
+ .ds-upload-act:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
522
+ @media (hover: none), (pointer: coarse) {
523
+ .ds-upload-act { min-height: 44px; min-width: 44px; }
524
+ /* 44px floor for file controls on ANY touch device, not only narrow ones
525
+ (the width-based mobile block misses wide touch tablets). */
526
+ .ds-file-act, .ds-file-sort-btn, .ds-crumb-seg, .ds-file-filter-input { min-height: 44px; }
527
+ .ds-file-act { min-width: 44px; }
528
+ .ds-file-check, .ds-file-selectall, .ds-density-btn { min-height: 44px; min-width: 44px; }
529
+ }
530
+
531
+ /* Empty state */
532
+ .ds-file-empty {
533
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
534
+ gap: var(--space-2); padding: var(--space-6) var(--space-3);
535
+ color: var(--fg-3); text-align: center;
536
+ }
537
+ .ds-file-empty-glyph { font-size: 34px; opacity: 0.55; }
538
+ .ds-file-empty-text { font-size: var(--fs-sm); }
539
+
540
+ /* -- Modals ------------------------------------------------- */
541
+ .ds-modal-backdrop {
542
+ position: fixed; inset: 0; z-index: var(--z-modal, 800);
543
+ display: flex; align-items: center; justify-content: center;
544
+ /* Notch/home-indicator safe: near-fullscreen modals must not slide under. */
545
+ padding: max(var(--space-3), env(safe-area-inset-top))
546
+ max(var(--space-3), env(safe-area-inset-right))
547
+ max(var(--space-3), env(safe-area-inset-bottom))
548
+ max(var(--space-3), env(safe-area-inset-left));
549
+ background: color-mix(in oklab, var(--ink) 55%, transparent);
550
+ }
551
+ @media (prefers-reduced-motion: no-preference) {
552
+ .ds-modal-backdrop { animation: ds-modal-fade var(--dur-base) var(--ease); }
553
+ }
554
+ @keyframes ds-modal-fade { from { opacity: 0; } to { opacity: 1; } }
555
+ .ds-modal {
556
+ display: flex; flex-direction: column;
557
+ width: 100%; max-width: 460px;
558
+ max-height: calc(100vh - 2 * var(--space-4));
559
+ max-height: calc(100dvh - 2 * var(--space-4));
560
+ background: var(--bg); color: var(--fg);
561
+ border: var(--bw-hair) solid var(--rule);
562
+ border-radius: var(--r-3); box-shadow: var(--shadow-3);
563
+ overflow: hidden;
564
+ }
565
+ @media (prefers-reduced-motion: no-preference) {
566
+ .ds-modal { animation: ds-modal-rise var(--dur-base) var(--ease); }
567
+ }
568
+ @keyframes ds-modal-rise {
569
+ from { opacity: 0; transform: translateY(8px) scale(0.98); }
570
+ to { opacity: 1; transform: none; }
571
+ }
572
+ .ds-modal-small { max-width: 380px; }
573
+ .ds-modal-preview { max-width: 880px; }
574
+ .ds-modal-head {
575
+ padding: var(--space-3) var(--space-4);
576
+ font-weight: 600; font-size: var(--fs-body);
577
+ border-bottom: var(--bw-hair) solid var(--rule);
578
+ }
579
+ .ds-modal-body { padding: var(--space-4); overflow: auto; font-size: var(--fs-sm); }
580
+ .ds-modal-actions {
581
+ display: flex; justify-content: flex-end; gap: var(--space-2);
582
+ padding: var(--space-3) var(--space-4);
583
+ border-top: var(--bw-hair) solid var(--rule);
584
+ }
585
+ .ds-modal-input {
586
+ width: 100%; padding: 10px 12px;
587
+ background: var(--bg-2); border: var(--bw-hair) solid var(--rule);
588
+ border-radius: var(--r-2); color: var(--fg);
589
+ font-family: inherit; font-size: var(--fs-sm);
590
+ }
591
+ .ds-modal-input:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
592
+ /* iOS auto-zooms inputs under 16px on focus, breaking the modal layout. */
593
+ @media (hover: none), (pointer: coarse) {
594
+ .ds-modal-input { font-size: 16px; min-height: 44px; }
595
+ }
596
+ /* PromptDialog's optional roots-chip row (destination-path prompts with more
597
+ than one allowed root) - same chip idiom the cwd editor's roots row uses. */
598
+ .ds-prompt-roots { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
599
+ .ds-prompt-root-chip {
600
+ background: var(--bg-2); border: var(--bw-hair) solid var(--rule); color: var(--fg-2);
601
+ border-radius: var(--r-pill); padding: 3px 10px; font: inherit; font-size: var(--fs-tiny);
602
+ cursor: pointer;
603
+ }
604
+ .ds-prompt-root-chip:hover { border-color: var(--accent); color: var(--fg); }
605
+ .ds-prompt-root-chip:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
606
+ @media (pointer: coarse) { .ds-prompt-root-chip { min-height: 44px; } }
607
+ /* In-body modal error (role=alert): mutation failures (409/403) surface INSIDE
608
+ the dialog, not behind the fixed overlay. */
609
+ .ds-modal-error {
610
+ margin: var(--space-2) 0 0; padding: 8px 10px;
611
+ border: var(--bw-hair) solid var(--flame); border-radius: var(--r-2);
612
+ background: color-mix(in oklab, var(--flame) 12%, var(--bg));
613
+ color: var(--flame); font-size: var(--fs-xs);
614
+ }
615
+ .btn-primary.danger {
616
+ background: var(--warn);
617
+ border-color: var(--warn);
618
+ color: var(--on-color);
619
+ }
620
+ .btn-primary.danger:hover { filter: brightness(0.92); border-color: var(--warn); }
621
+
622
+ /* -- Preview ------------------------------------------------ */
623
+ .ds-preview-head {
624
+ display: flex; align-items: center; gap: var(--space-3);
625
+ padding: var(--space-3) var(--space-4);
626
+ border-bottom: var(--bw-hair) solid var(--rule);
627
+ }
628
+ .ds-preview-name {
629
+ font-weight: 600; font-size: var(--fs-body);
630
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
631
+ flex: 1 1 auto;
632
+ }
633
+ .ds-preview-meta {
634
+ font-family: var(--ff-mono); font-size: var(--fs-xs);
635
+ color: var(--fg-3); white-space: nowrap;
636
+ flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
637
+ }
638
+ .ds-preview-actions { display: inline-flex; gap: 2px; margin-left: auto; }
639
+ .ds-preview-body {
640
+ flex: 1; min-height: 0; overflow: auto;
641
+ padding: var(--space-3) var(--space-4);
642
+ display: flex; flex-direction: column;
643
+ }
644
+ .ds-preview-media {
645
+ max-width: 100%;
646
+ max-height: 70vh; max-height: 70dvh;
647
+ margin: 0 auto;
648
+ border-radius: var(--r-2); object-fit: contain;
649
+ }
650
+ .ds-preview-audio { width: 100%; }
651
+ .ds-preview-code,
652
+ .ds-preview-text {
653
+ margin: 0; flex: 1; min-height: 0; overflow: auto;
654
+ padding: var(--space-3);
655
+ background: var(--ink); color: var(--paper);
656
+ border-radius: var(--r-2);
657
+ font-family: var(--ff-mono); font-size: var(--fs-xs); line-height: var(--lh-base);
658
+ white-space: pre; tab-size: 2;
659
+ }
660
+ .ds-preview-text { white-space: pre-wrap; word-break: break-word; }
661
+ .ds-preview-code code, .ds-preview-text code { font: inherit; color: inherit; background: none; }
662
+ .ds-preview-truncated {
663
+ margin-top: var(--space-2); padding-top: var(--space-2);
664
+ border-top: var(--bw-hair) solid color-mix(in oklab, var(--paper) 20%, transparent);
665
+ color: var(--paper-3); font-size: var(--fs-micro);
666
+ }
667
+ .ds-preview-fallback {
668
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
669
+ gap: var(--space-2); padding: var(--space-6); color: var(--fg-3);
670
+ }
671
+ .ds-preview-glyph { font-size: 34px; opacity: 0.6; }
672
+
673
+ /* Thin scrollbars inside preview bodies. */
674
+ .ds-preview-code, .ds-preview-text, .ds-modal-body, .ds-preview-body {
675
+ scrollbar-width: thin;
676
+ }
677
+ .ds-preview-code::-webkit-scrollbar,
678
+ .ds-preview-text::-webkit-scrollbar,
679
+ .ds-preview-body::-webkit-scrollbar,
680
+ .ds-modal-body::-webkit-scrollbar { width: 10px; height: 10px; }
681
+ .ds-preview-code::-webkit-scrollbar-thumb,
682
+ .ds-preview-text::-webkit-scrollbar-thumb {
683
+ background: color-mix(in oklab, var(--paper) 28%, transparent);
684
+ border-radius: var(--r-pill); border: 2px solid transparent; background-clip: padding-box;
685
+ }
686
+ .ds-preview-body::-webkit-scrollbar-thumb,
687
+ .ds-modal-body::-webkit-scrollbar-thumb {
688
+ background: var(--bg-3); border-radius: var(--r-pill);
689
+ }
690
+
691
+ /* Mobile — modals go near-fullscreen, preview media shrinks. */
692
+ @media (max-width: 560px) {
693
+ .ds-modal {
694
+ max-width: 100%;
695
+ max-height: calc(100vh - 2 * var(--space-3));
696
+ max-height: calc(100dvh - 2 * var(--space-3));
697
+ }
698
+ .ds-modal-preview {
699
+ width: 100%; max-height: none;
700
+ height: calc(100vh - 2 * var(--space-3));
701
+ height: calc(100dvh - 2 * var(--space-3));
702
+ }
703
+ .ds-preview-media { max-height: 50vh; max-height: 50dvh; }
704
+ .ds-file-stage { padding: var(--space-3) var(--space-2); }
705
+ /* Preview head: let it wrap and drop the nowrap meta (duplicated in body)
706
+ so the close button can never be pushed past the modal edge. */
707
+ .ds-preview-head { flex-wrap: wrap; row-gap: 4px; }
708
+ .ds-preview-meta { display: none; }
709
+ }