@stonecrop/desktop 0.31.0 → 0.33.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/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  A three-view UI shell for Stonecrop applications. Renders a doctype list → records list → record form layout driven by the host application's Registry and HST state.
4
4
 
5
- Desktop reads through Stonecrop — on navigating to a list or a record it calls `Stonecrop.getRecords` / `Stonecrop.getRecord`, which fetch through the host's registered `DataClient` and write into HST. Writes are the host's: Desktop emits `action` and the host dispatches it.
5
+ Desktop reads through Stonecrop: on navigating to a list or a record it calls `Stonecrop.getRecords` / `Stonecrop.getRecord`, which fetch through the host's registered `DataClient` and write into HST. Writes are the host's: Desktop emits `action` and the host dispatches it.
6
6
 
7
7
  ## Features
8
8
 
9
9
  - **Three-view layout**: doctypes → records → record form, navigated by route or adapter
10
10
  - **ActionSet toolbar**: FSM transitions become action buttons/dropdowns automatically from the doctype workflow
11
11
  - **CommandPalette**: `Ctrl+K` / `Cmd+K` search across doctypes and records
12
- - **SheetNav**: tabbed navigation between open records
12
+ - **SheetNav**: tabbed navigation between open records, with a toolbar for host controls in the footer (see [Slots](#slots))
13
13
  - **Event-driven**: all significant interactions emit typed events for the host to respond to
14
14
 
15
15
  ## Installation
@@ -28,7 +28,7 @@ import planDoctype from './doctypes/plan.json'
28
28
 
29
29
  const app = createApp(App)
30
30
 
31
- // The plugin constructs the Registry itself and provides it as `$registry` — it does not
31
+ // The plugin constructs the Registry itself and provides it as `$registry`; it does not
32
32
  // accept one. Register doctypes on that instance, after install.
33
33
  app.use(Stonecrop, { router, client: new RestDataClient() })
34
34
 
@@ -38,8 +38,7 @@ registry.addDoctype(Doctype.fromObject(planDoctype))
38
38
  app.mount('#app')
39
39
  ```
40
40
 
41
- `client` is the `DataClient` Desktop reads through. It can also be supplied later with
42
- `stonecrop.setClient(client)` — Nuxt hosts do this from a plugin via `useStonecropSetup().registerClient`.
41
+ `client` is the `DataClient` Desktop reads through. It can also be supplied later with `stonecrop.setClient(client)`; Nuxt hosts do this from a plugin via `useStonecropSetup().registerClient`.
43
42
 
44
43
  ## Basic Usage
45
44
 
@@ -50,7 +49,7 @@ import { useClientAction } from '@stonecrop/stonecrop'
50
49
 
51
50
  // Runs an action's clientHandler when the doctype declares one, dispatches to the server
52
51
  // otherwise, and reconciles the store and the route with the identity the server settled on.
53
- // In a Nuxt host this is auto-imported — drop the import line.
52
+ // In a Nuxt host this is auto-imported, so drop the import line.
54
53
  const { run } = useClientAction()
55
54
  </script>
56
55
 
@@ -67,27 +66,47 @@ const { run } = useClientAction()
67
66
  | Prop | Type | Default | Description |
68
67
  |------|------|---------|-------------|
69
68
  | `availableDoctypes` | `string[]` | `[]` | Doctype slugs to display in the doctypes list |
70
- | `routeAdapter` | `RouteAdapter` | — | Custom routing layer (required for Nuxt/custom hosts) |
69
+ | `routeAdapter` | `RouteAdapter` | none | Custom routing layer (required for Nuxt/custom hosts) |
71
70
 
72
- Record identity is not a prop. It is declared per doctype (`primaryKey`, falling back to `id`) and
73
- resolved through `Doctype.getRecordId`, so a row's link always matches the key the record is stored
74
- under. One shell renders many doctypes, so a single prop could never answer this correctly.
71
+ Record identity is not a prop. It is declared per doctype (`primaryKey`, falling back to `id`) and resolved through `Doctype.getRecordId`, so a row's link always matches the key the record is stored under. One shell renders many doctypes, so a single prop could never answer this correctly.
72
+
73
+ ## Slots
74
+
75
+ | Slot | Renders |
76
+ |------|---------|
77
+ | `sheetnav-toolbar` | Host controls in the footer, immediately left of the navigation tabs |
78
+
79
+ A control deeper in the page than this slot can reach, such as a custom field inside the record form, teleports into the same place:
80
+
81
+ ```vue
82
+ <script setup lang="ts">
83
+ import { SHEET_NAV_TOOLBAR_SELECTOR } from '@stonecrop/desktop'
84
+ </script>
85
+
86
+ <template>
87
+ <Teleport :to="SHEET_NAV_TOOLBAR_SELECTOR">
88
+ <MyToolbar />
89
+ </Teleport>
90
+ </template>
91
+ ```
92
+
93
+ Mount one SheetNav per page: with two, teleported content lands in the first and SheetNav logs a warning. When the toolbar and the tabs do not fit on one row, the toolbar moves to a row above the tabs. The Nuxt playground's country records (`/country/US`) carry a working example.
75
94
 
76
95
  ## Emitted Events
77
96
 
78
97
  | Event | When |
79
98
  |-------|------|
80
- | `action` | User triggers a declared action — an FSM transition or a Command |
99
+ | `action` | User triggers a declared action: an FSM transition or a Command |
81
100
  | `navigate` | Desktop wants to change views |
82
101
  | `record:open` | User opens a specific record |
83
- | `load-records` | Desktop is about to read a records list (notification — Desktop performs the read) |
84
- | `load-record` | Desktop is about to read a single record (notification — Desktop performs the read) |
102
+ | `load-records` | Desktop is about to read a records list (a notification: Desktop performs the read) |
103
+ | `load-record` | Desktop is about to read a single record (a notification: Desktop performs the read) |
85
104
 
86
105
  See [api.md](./api.md) for payload type definitions.
87
106
 
88
107
  ### Event Handling Notes
89
108
 
90
- - **action**: Desktop merges `Doctype.getAvailableTransitions` and `Doctype.getAvailableCommands`, both resolved against `Stonecrop.getRecordState`, into one Actions dropdown. **Desktop never dispatches** — that is the host application's responsibility.
109
+ - **action**: Desktop merges `Doctype.getAvailableTransitions` and `Doctype.getAvailableCommands`, both resolved against `Stonecrop.getRecordState`, into one Actions dropdown. **Desktop never dispatches**: that is the host application's responsibility.
91
110
  - **load-records / load-record**: notifications, not fetch requests. Desktop reads through `Stonecrop.getRecords` / `Stonecrop.getRecord` itself, using the registered `DataClient`; these events announce that read so a host can hang analytics off it. A host that fetches here races Desktop's own read into the same HST key. `load-record` is not emitted for a draft, which has nothing to fetch.
92
111
 
93
112
  ## Router Adapter
@@ -125,13 +144,9 @@ function useCustomRouteAdapter(): RouteAdapter {
125
144
 
126
145
  ## Handling `action` Events
127
146
 
128
- Dispatching is not the whole job: the result has to land in HST under the identity the *server*
129
- settled on, which for a newly created record is not the id that was dispatched.
147
+ Dispatching is not the whole job: the result has to land in HST under the identity the *server* settled on, which for a newly created record is not the id that was dispatched.
130
148
 
131
- Bind `@action` to `useClientAction`'s `run`, as in Basic Usage above. It runs an action's
132
- `clientHandler` when it has one, dispatches otherwise, and reconciles the store and the route. It
133
- lives in `@stonecrop/stonecrop`, so every Vue 3 host gets the same one; Nuxt hosts also get it as an
134
- auto-import from `@stonecrop/nuxt`.
149
+ Bind `@action` to `useClientAction`'s `run`, as in Basic Usage above. It runs an action's `clientHandler` when it has one, dispatches otherwise, and reconciles the store and the route. It lives in `@stonecrop/stonecrop`, so every Vue 3 host gets the same one; Nuxt hosts also get it as an auto-import from `@stonecrop/nuxt`.
135
150
 
136
151
  Three things are adjustable, for the cases that genuinely differ between applications:
137
152
 
@@ -141,18 +156,11 @@ Three things are adjustable, for the cases that genuinely differ between applica
141
156
  | `followRecord` | `router.replace('/{doctype}/{id}')` | a locale prefix, a nested route, or staying put |
142
157
  | `onError` | a blocking `window.alert` | your own notification system |
143
158
 
144
- `args` is an opaque JSON array: nothing validates it, so both ends of your own stack have to agree.
145
- A backend taking positional `[recordId, data]` supplies `buildArgs` to say so.
159
+ `args` is an opaque JSON array: nothing validates it, so both ends of your own stack have to agree. A backend taking positional `[recordId, data]` supplies `buildArgs` to say so.
146
160
 
147
- Resolving a record's identity and keying it into HST are deliberately **not** adjustable. That rule
148
- is declared on the doctype and re-derived server-side by the adapter, and every host that re-derived
149
- it client-side got it wrong. If you dispatch through `Stonecrop.dispatchAction` directly instead of
150
- using this composable, that method still files the returned record under the settled identity — you
151
- cannot store it under the wrong key by accident. What you lose is the stale-key cleanup and the
152
- route-follow, which need the id you dispatched.
161
+ Resolving a record's identity and keying it into HST are deliberately **not** adjustable. That rule is declared on the doctype and re-derived server-side by the adapter, and every host that re-derived it client-side got it wrong. If you dispatch through `Stonecrop.dispatchAction` directly instead of using this composable, that method still files the returned record under the settled identity, so you cannot store it under the wrong key by accident. What you lose is the stale-key cleanup and the route-follow, which need the id you dispatched.
153
162
 
154
- Do not copy form data into HST before dispatching. Desktop already hands you the current form
155
- snapshot in `payload.data`, and an unsaved record has no HST node to write to.
163
+ Do not copy form data into HST before dispatching. Desktop already hands you the current form snapshot in `payload.data`, and an unsaved record has no HST node to write to.
156
164
 
157
165
  See the [host integration guide](../nuxt/documentation/content/guides/desktop-integration.md) for the full wiring.
158
166
 
@@ -169,4 +177,4 @@ const { navigateToDoctype, openRecord, createNewRecord, emitAction } =
169
177
 
170
178
  `emitAction(name, data?)` is a convenience wrapper for emitting an `action` event from deeply nested slot content without passing refs down manually.
171
179
 
172
- Desktop blesses no action name. It used to expose a `handleDelete` method and a `confirmFn` prop, which together emitted a hardcoded `DELETE` action and prompted before it — but no doctype declares `DELETE`, so it failed on every click, and only the host knows which of its actions are destructive. Removal is a workflow outcome: declare an action with a `nextState` such as `Archived` or `CANCELLED`, and confirm inside your own `@action` handler before dispatching.
180
+ Desktop blesses no action name. It used to expose a `handleDelete` method and a `confirmFn` prop, which together emitted a hardcoded `DELETE` action and prompted before it; but no doctype declares `DELETE`, so it failed on every click, and only the host knows which of its actions are destructive. Removal is a workflow outcome: declare an action with a `nextState` such as `Archived` or `CANCELLED`, and confirm inside your own `@action` handler before dispatching.
@@ -0,0 +1,410 @@
1
+
2
+ #cross[data-v-8e18f635] {
3
+ position: relative;
4
+ transform: rotate(45deg);
5
+ cursor: pointer;
6
+ transition: all 0.2s ease-in-out;
7
+ user-select: none;
8
+ line-height: 1rem;
9
+ }
10
+ #cross.rotated[data-v-8e18f635],
11
+ .cross.rotated[data-v-8e18f635] {
12
+ transform: rotate(0deg);
13
+ }
14
+ #cross svg[data-v-8e18f635] {
15
+ width: 1.5em;
16
+ height: 1.5em;
17
+ }
18
+ .action-set[data-v-8e18f635] {
19
+ position: fixed;
20
+ top: 300px;
21
+ right: 10px;
22
+ padding: 10px;
23
+ display: flex;
24
+ flex-direction: column;
25
+ align-items: flex-end;
26
+ background: var(--sc-form-background);
27
+ border: 1px solid var(--sc-gray-20);
28
+ border-left: 4px solid var(--sc-gray-20);
29
+ border-radius: 0;
30
+ overflow: hidden;
31
+ z-index: 1001; /* Above SheetNav (100) and operation log button (999) */
32
+ -webkit-transition: all 0.5s ease-in-out;
33
+ -moz-transition: all 0.5s ease-in-out;
34
+ -o-transition: all 0.5s ease-in-out;
35
+ transition: all 0.5s ease-in-out;
36
+ }
37
+ .action-menu-icon[data-v-8e18f635] {
38
+ position: relative;
39
+ font-size: 2rem;
40
+ display: inline-block;
41
+ color: var(--sc-gray-60);
42
+ transition: all 0.2s ease-in-out;
43
+ }
44
+ .action-set.collapsed[data-v-8e18f635] {
45
+ max-width: 46px;
46
+ max-height: 40px;
47
+ overflow: hidden;
48
+ }
49
+ .action-set.collapsed .action-element[data-v-8e18f635] {
50
+ opacity: 0;
51
+ -webkit-transition: opacity 0.25s ease-in-out;
52
+ -moz-transition: opacity 0.25s ease-in-out;
53
+ -o-transition: opacity 0.25s ease-in-out;
54
+ transition: opacity 0.25s ease-in-out;
55
+ }
56
+ .action-element[data-v-8e18f635] {
57
+ width: 100%;
58
+ text-align: right;
59
+ border: 1px solid var(--sc-gray-20);
60
+ background: none;
61
+ font-size: 1.5rem;
62
+ font-family: var(--sc-font-family);
63
+ font-weight: 600;
64
+ margin-top: 10px;
65
+ position: relative; /* Make this the positioning context for absolute children */
66
+ }
67
+ .action-element-header[data-v-8e18f635] {
68
+ display: flex;
69
+ justify-content: end;
70
+ }
71
+ button.button-default[data-v-8e18f635] {
72
+ background-color: transparent;
73
+ padding: 5px 12px;
74
+ border-radius: 0px;
75
+ box-shadow: none;
76
+ border: none;
77
+ cursor: pointer;
78
+ white-space: nowrap;
79
+ font-weight: bold;
80
+ font-size: 1rem;
81
+ text-align: right;
82
+ color: var(--sc-gray-80);
83
+ padding-left: 50px;
84
+ font-family: var(--sc-font-family);
85
+ }
86
+ .dropdown-header:hover button.button-default[data-v-8e18f635],
87
+ .dropdown-header[data-v-8e18f635]:hover,
88
+ .action-element-header[data-v-8e18f635]:hover {
89
+ background-color: #f2f2f2;
90
+ }
91
+ .dropdown-title[data-v-8e18f635] {
92
+ position: relative;
93
+ }
94
+ .dropdown-header[data-v-8e18f635] {
95
+ display: flex;
96
+ align-items: center;
97
+ }
98
+ .cross[data-v-8e18f635] {
99
+ pointer-events: all;
100
+ margin-left: 5px;
101
+ font-family: var(--sc-font-family);
102
+ color: var(--sc-gray-60);
103
+ cursor: pointer;
104
+ transform: rotate(45deg);
105
+ user-select: none;
106
+ transition: all 0.2s ease-in-out;
107
+ line-height: 1rem;
108
+ }
109
+ button.dropdown-item[data-v-8e18f635] {
110
+ width: 100%;
111
+ padding: 5px 12px;
112
+ text-align: right;
113
+ border: none;
114
+ background-color: #ffffff;
115
+ cursor: pointer;
116
+ border-radius: 5px;
117
+ font-size: 1rem;
118
+ }
119
+ button.dropdown-item[data-v-8e18f635]:hover {
120
+ background-color: #f2f2f2;
121
+ }
122
+
123
+ .fade-enter-active,
124
+ .fade-leave-active {
125
+ transition: opacity 0.2s ease;
126
+ }
127
+ .fade-enter-from,
128
+ .fade-leave-to {
129
+ opacity: 0;
130
+ }
131
+ .command-palette-overlay {
132
+ position: fixed;
133
+ top: 0;
134
+ left: 0;
135
+ width: 100%;
136
+ height: 100%;
137
+ background-color: rgba(0, 0, 0, 0.5);
138
+ display: flex;
139
+ align-items: flex-start;
140
+ justify-content: center;
141
+ z-index: 300;
142
+ padding-top: 100px;
143
+ }
144
+ .command-palette {
145
+ width: 600px;
146
+ max-width: 90%;
147
+ background-color: white;
148
+ border-radius: 8px;
149
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
150
+ overflow: hidden;
151
+ max-height: 80vh;
152
+ display: flex;
153
+ flex-direction: column;
154
+ }
155
+ .command-palette-header {
156
+ display: flex;
157
+ border-bottom: 1px solid #eaeaea;
158
+ padding: 12px;
159
+ }
160
+ .command-palette-input {
161
+ flex: 1;
162
+ border: none;
163
+ outline: none;
164
+ font-size: 16px;
165
+ padding: 8px 12px;
166
+ background-color: transparent;
167
+ }
168
+ .command-palette-close {
169
+ background: transparent;
170
+ border: none;
171
+ font-size: 24px;
172
+ cursor: pointer;
173
+ color: #666;
174
+ padding: 0 8px;
175
+ }
176
+ .command-palette-close:hover {
177
+ color: #333;
178
+ }
179
+ .command-palette-results {
180
+ overflow-y: auto;
181
+ max-height: 60vh;
182
+ }
183
+ .command-palette-result {
184
+ padding: 12px 16px;
185
+ cursor: pointer;
186
+ border-bottom: 1px solid #f0f0f0;
187
+ }
188
+ .command-palette-result:hover,
189
+ .command-palette-result.selected {
190
+ background-color: #f5f5f5;
191
+ }
192
+ .command-palette-result.selected {
193
+ background-color: rgba(132, 60, 3, 0.1);
194
+ }
195
+ .result-title {
196
+ font-weight: 500;
197
+ margin-bottom: 4px;
198
+ color: #333;
199
+ }
200
+ .result-content {
201
+ font-size: 14px;
202
+ color: #666;
203
+ white-space: nowrap;
204
+ overflow: hidden;
205
+ text-overflow: ellipsis;
206
+ }
207
+ .command-palette-no-results {
208
+ padding: 20px 16px;
209
+ text-align: center;
210
+ color: #666;
211
+ }
212
+
213
+ /* Pinned to both edges: without `left`, a fixed box takes its container's offset and overflows the
214
+ viewport. Only the toolbar and the tabs take pointer events; the empty strip passes clicks through. */
215
+ footer[data-v-7c905747] {
216
+ position: fixed;
217
+ right: 0;
218
+ bottom: 0;
219
+ left: 0;
220
+ pointer-events: none;
221
+ background-color: transparent;
222
+ z-index: 100;
223
+ text-align: left;
224
+ font-size: 100%;
225
+ display: flex;
226
+ justify-content: flex-end;
227
+ align-items: flex-end;
228
+ padding: 0 0.75rem 0 0;
229
+ box-sizing: border-box;
230
+ }
231
+
232
+ /* When the toolbar and the tabs do not fit on one row, the toolbar wraps onto its own row above. */
233
+ .sheetnav-footer-cluster[data-v-7c905747] {
234
+ display: flex;
235
+ flex-wrap: wrap;
236
+ justify-content: flex-end;
237
+ align-items: flex-end;
238
+ column-gap: 0.5rem;
239
+ max-width: 100%;
240
+ min-width: 0;
241
+ }
242
+ .sheetnav-toolbar[data-v-7c905747] {
243
+ pointer-events: auto;
244
+ min-width: 0;
245
+ min-height: 2.4rem;
246
+ display: flex;
247
+ flex-wrap: wrap;
248
+ justify-content: flex-end;
249
+ align-items: center;
250
+ }
251
+ .tabs[data-v-7c905747] {
252
+ pointer-events: auto;
253
+ flex: 0 0 auto;
254
+ display: flex;
255
+ flex-direction: row-reverse;
256
+ align-items: stretch;
257
+ height: 2.4rem;
258
+ margin: 0;
259
+ padding: 0;
260
+ list-style: none;
261
+ }
262
+ .tabs li[data-v-7c905747] {
263
+ display: flex;
264
+ align-items: stretch;
265
+ list-style-type: none;
266
+ position: relative;
267
+ margin-left: -1px;
268
+ }
269
+
270
+ /* Base tab styling */
271
+ .tabs a[data-v-7c905747] {
272
+ height: 100%;
273
+ min-height: 2.4rem;
274
+ padding: 0 1rem;
275
+ box-sizing: border-box;
276
+ display: flex;
277
+ align-items: center;
278
+ justify-content: center;
279
+ text-decoration: none;
280
+ color: var(--sc-gray-60);
281
+ background: var(--sc-btn-color);
282
+ border: 1px solid var(--sc-btn-border);
283
+ font-size: 0.85rem;
284
+ font-family: var(--sc-font-family);
285
+ transition: all 0.15s ease;
286
+
287
+ /* Minimal ornamentation - subtle top radius only */
288
+ border-top-left-radius: 2px;
289
+ border-top-right-radius: 2px;
290
+ }
291
+ .tabs a[data-v-7c905747]:hover {
292
+ background: var(--sc-btn-hover);
293
+ }
294
+ .tabs .router-link-active[data-v-7c905747] {
295
+ z-index: 3;
296
+ background: var(--sc-primary-color) !important;
297
+ border-color: var(--sc-primary-color) !important;
298
+ color: var(--sc-primary-text-color) !important;
299
+ }
300
+
301
+ /* Pseudo-elements removed - minimal ornamentation style */
302
+
303
+ /* Hide breadcrumbs tab */
304
+ .hidebreadcrumbs a[data-v-7c905747] {
305
+ min-width: 2.4rem;
306
+ width: 2.4rem;
307
+ padding: 0.5rem;
308
+ }
309
+ .hidebreadcrumbs a div[data-v-7c905747] {
310
+ font-size: 1.2rem;
311
+ }
312
+ .rotated[data-v-7c905747] {
313
+ transform: rotate(45deg);
314
+ -webkit-transform: rotate(45deg);
315
+ -moz-transform: rotate(45deg);
316
+ -ms-transform: rotate(45deg);
317
+ -o-transform: rotate(45deg);
318
+ transition: transform 250ms ease;
319
+ }
320
+ .unrotated[data-v-7c905747] {
321
+ transform: rotate(0deg);
322
+ -webkit-transform: rotate(0deg);
323
+ -moz-transform: rotate(0deg);
324
+ -ms-transform: rotate(0deg);
325
+ -o-transform: rotate(0deg);
326
+ transition: transform 250ms ease;
327
+ }
328
+ li[data-v-7c905747]:active,
329
+ li[data-v-7c905747]:hover,
330
+ li[data-v-7c905747]:focus,
331
+ li > a[data-v-7c905747]:active,
332
+ li > a[data-v-7c905747]:hover,
333
+ li > a[data-v-7c905747]:focus {
334
+ z-index: 3;
335
+ }
336
+ a[data-v-7c905747]:active,
337
+ a[data-v-7c905747]:hover,
338
+ a[data-v-7c905747]:focus {
339
+ outline: 2px solid var(--sc-input-active-border-color);
340
+ z-index: 3;
341
+ }
342
+
343
+ /* Home tab */
344
+ .hometab a[data-v-7c905747] {
345
+ min-width: 2.4rem;
346
+ width: 2.4rem;
347
+ padding: 0.5rem;
348
+ }
349
+
350
+ /* SVG icon styling */
351
+ .icon[data-v-7c905747] {
352
+ width: 1rem;
353
+ height: 1rem;
354
+ stroke: currentColor;
355
+ flex-shrink: 0;
356
+ }
357
+
358
+ /* Search tab with animation */
359
+ .searchtab[data-v-7c905747] {
360
+ overflow: hidden;
361
+ }
362
+ .searchtab a[data-v-7c905747] {
363
+ min-width: 2.4rem;
364
+ padding: 0.5rem;
365
+ overflow: hidden;
366
+ /* Animation for smooth expand/collapse */
367
+ max-width: 2.4rem;
368
+ transition:
369
+ max-width 0.35s ease-in-out,
370
+ padding 0.35s ease-in-out,
371
+ background 0.2s ease;
372
+ }
373
+ .searchtab .search-icon[data-v-7c905747] {
374
+ cursor: pointer;
375
+ }
376
+ .searchtab input[data-v-7c905747] {
377
+ outline: none;
378
+ border: 1px solid var(--sc-input-border-color);
379
+ border-radius: 0.25rem;
380
+ background-color: var(--sc-form-background);
381
+ color: var(--sc-gray-80);
382
+ text-align: left;
383
+ font-size: 0.875rem;
384
+ padding: 0.25rem 0.5rem;
385
+ width: 180px;
386
+ height: 1.5rem;
387
+ flex-shrink: 0;
388
+ opacity: 0;
389
+ transition: opacity 0.2s ease-in-out;
390
+ transition-delay: 0s;
391
+ }
392
+ .searchtab input[data-v-7c905747]:focus {
393
+ border-color: var(--sc-input-active-border-color);
394
+ outline: none;
395
+ }
396
+ .searchtab input[data-v-7c905747]::placeholder {
397
+ color: var(--sc-input-label-color);
398
+ }
399
+
400
+ /* Search active state - expanded with animation */
401
+ .searchtab.search-active a[data-v-7c905747] {
402
+ max-width: 200px;
403
+ min-width: auto;
404
+ width: auto;
405
+ padding: 0.5rem 0.75rem;
406
+ }
407
+ .searchtab.search-active input[data-v-7c905747] {
408
+ opacity: 1;
409
+ transition-delay: 0.15s;
410
+ }