@lapismd/design-core 0.1.0 → 0.1.2

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 (67) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/package.json +12 -8
  4. package/spec/src/styling-and-themes.md +9 -9
  5. package/src/shared/ai/chat.css +2 -1
  6. package/src/shared/ai/composer-input/ComposerInput.svelte +3 -2
  7. package/src/shared/ai/experimental/reaction-bar/ReactionBar.svelte +1 -1
  8. package/src/shared/ai/tool-calls/ToolCalls.svelte +1 -1
  9. package/src/shared/ai/trigger-menu.ts +2 -3
  10. package/src/shared/diff/file-diff/FileDiff.svelte +5 -8
  11. package/src/shared/diff/file-listing/FileListingViewModeToggle.css +42 -5
  12. package/src/shared/diff/merge-editor/MergeEditor.svelte +2 -7
  13. package/src/shared/filter/filter-query/filters.ts +2 -2
  14. package/src/shared/filter/filter-query/parser.ts +9 -8
  15. package/src/shared/filter/power-search/PowerSearch.svelte +1 -1
  16. package/src/shared/filter/search-filter-bar/SearchFilterBar.svelte +2 -2
  17. package/src/shared/filter/search-filter-bar/SearchFilterInput.svelte +1 -1
  18. package/src/shared/filter/search-filter-bar/SearchFilterPredicateEditor.svelte +1 -1
  19. package/src/shared/filter/search-filter-bar/search-filter-syntax.ts +1 -1
  20. package/src/shared/forms/autocomplete-input/AutocompleteInput.svelte +19 -7
  21. package/src/shared/shadcn/command/command-dialog.svelte +1 -1
  22. package/src/shared/shadcn/command/command-group.svelte +3 -1
  23. package/src/shared/shadcn/command-view/command-view.svelte +1 -6
  24. package/src/shared/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -691
  25. package/src/shared/shadcn/dropdown-menu/dropdown-menu-content.svelte +691 -0
  26. package/src/shared/shadcn/dropdown-menu/dropdown-menu-radio-group.svelte +1 -1
  27. package/src/shared/shadcn/scroll-area/scroll-area.svelte +2 -0
  28. package/src/shared/shadcn/spinner/spinner.svelte +3 -3
  29. package/src/shared/shell/app-shell/AppShell.demo.css +37 -0
  30. package/src/shared/shell/app-shell/AppShellBodyContent.svelte +5 -1
  31. package/src/shared/shell/app-shell/AppShellBodySidebar.svelte +5 -1
  32. package/src/shared/shell/app-shell/AppShellMobilePanelSelector.svelte +1 -1
  33. package/src/shared/shell/app-shell/app-shell-controller.svelte.ts +23 -9
  34. package/src/shared/shell/app-shell/examples/AppShellBodyDemo.svelte +4 -0
  35. package/src/shared/shell/app-shell/examples/AppShellCompleteDemo.svelte +1 -0
  36. package/src/shared/shell/app-shell/examples/AppShellConversationDemo.svelte +1 -0
  37. package/src/shared/shell/app-shell/examples/AppShellExamples.css +521 -0
  38. package/src/shared/shell/app-shell/examples/AppShellFilesSidebarDemo.svelte +1 -0
  39. package/src/shared/shell/app-shell/examples/AppShellMarkdownDocumentDemo.svelte +6 -1
  40. package/src/shared/shell/app-shell/examples/AppShellMarkdownFilesDemo.svelte +1 -0
  41. package/src/shared/shell/app-shell/examples/AppShellModeSwitchDemo.svelte +1 -0
  42. package/src/shared/shell/app-shell/examples/AppShellProjectSidebarDemo.svelte +1 -0
  43. package/src/shared/shell/app-shell/examples/AppShellSidebarDemo.svelte +1 -0
  44. package/src/shared/shell/app-shell/examples/AppShellToolbarDemo.svelte +1 -0
  45. package/src/shared/workspace/menu/WorkspaceContextMenuItems.svelte +2 -1
  46. package/src/shared/workspace/menu/WorkspaceMenuItems.svelte +2 -1
  47. package/src/shared/workspace/problems/ProblemsQuickFix.svelte +1 -4
  48. package/src/shared/workspace/settings/AppSettingsContent.svelte +6 -0
  49. package/src/shared/workspace/settings/AppSettingsNavigation.svelte +7 -0
  50. package/src/shared/workspace/settings/CopyableValue.svelte +81 -0
  51. package/src/shared/workspace/settings/DeclarativeSettingsSection.svelte +20 -0
  52. package/src/shared/workspace/settings/WorkspaceSettingField.svelte +110 -41
  53. package/src/shared/workspace/settings/WorkspaceSettingMultiSelect.svelte +7 -6
  54. package/src/shared/workspace/settings/WorkspaceSettingObjectTable.svelte +187 -23
  55. package/src/shared/workspace/settings/WorkspaceSettingSelect.svelte +29 -29
  56. package/src/shared/workspace/settings/WorkspaceSettings.css +190 -4
  57. package/src/shared/workspace/settings/WorkspaceSettingsStoryCustomField.svelte +2 -2
  58. package/src/shared/workspace/settings/WorkspaceSettingsStoryCustomPage.svelte +13 -0
  59. package/src/shared/workspace/settings/WorkspaceSettingsSurface.svelte +50 -0
  60. package/src/shared/workspace/settings/configuration.ts +5 -0
  61. package/src/shared/workspace/settings/copyable-value.ts +10 -0
  62. package/src/shared/workspace/settings/index.ts +5 -0
  63. package/src/shared/workspace/settings/settings-controller.svelte.ts +349 -15
  64. package/src/shared/workspace/settings/types.ts +90 -8
  65. package/src/storybook/app-shell-reference/AppShellTwoExpandedSidebarsReference.css +25 -0
  66. package/src/storybook/app-shell-reference/AppShellTwoExpandedSidebarsReference.svelte +48 -0
  67. package/src/storybook/app-shell-reference/index.ts +1 -0
@@ -5,13 +5,17 @@
5
5
  import * as Table from "@lapismd/design-core/shadcn/table";
6
6
  import WorkspaceIcon from "../icon/WorkspaceIcon.svelte";
7
7
  import WorkspaceSettingAddButton from "./WorkspaceSettingAddButton.svelte";
8
+ import CopyableValue from "./CopyableValue.svelte";
8
9
  import {
9
10
  asObjectMap,
10
11
  asObjectRows,
11
12
  createObjectRow,
12
13
  nextObjectMapKey,
13
14
  } from "./object-collection.js";
14
- import type { WorkspaceObjectProperty } from "./types.js";
15
+ import type {
16
+ WorkspaceObjectProperty,
17
+ WorkspaceObjectRowAction,
18
+ } from "./types.js";
15
19
 
16
20
  let {
17
21
  label,
@@ -22,6 +26,12 @@
22
26
  minimumItems,
23
27
  maximumItems,
24
28
  addLabel,
29
+ rowKey,
30
+ reorderable = false,
31
+ allowAdd = true,
32
+ allowRemove = true,
33
+ stateRevision = 0,
34
+ rowActions = [],
25
35
  onValueChange,
26
36
  }: {
27
37
  label: string;
@@ -32,9 +42,23 @@
32
42
  minimumItems?: number;
33
43
  maximumItems?: number;
34
44
  addLabel?: string;
45
+ rowKey?: string;
46
+ reorderable?: boolean;
47
+ allowAdd?: boolean;
48
+ allowRemove?: boolean;
49
+ stateRevision?: number;
50
+ rowActions?: WorkspaceObjectRowAction[];
35
51
  onValueChange: (value: unknown) => void;
36
52
  } = $props();
37
53
 
54
+ let busyActions = $state<Record<string, boolean>>({});
55
+ let actionResults = $state<
56
+ Record<string, { tone: "success" | "warning" | "error"; message: string }>
57
+ >({});
58
+ let visibleProperties = $derived(
59
+ properties.filter((property) => property.presentation !== "hidden"),
60
+ );
61
+
38
62
  let rows = $derived.by(() => {
39
63
  if (mode === "map") {
40
64
  return Object.entries(asObjectMap(value)).map(([key, row]) => ({
@@ -43,15 +67,22 @@
43
67
  }));
44
68
  }
45
69
  return asObjectRows(value).map((row, index) => ({
46
- key: String(index),
70
+ key:
71
+ rowKey && typeof row[rowKey] === "string" && String(row[rowKey]).trim()
72
+ ? String(row[rowKey])
73
+ : String(index),
47
74
  row,
48
75
  }));
49
76
  });
50
77
  let canAdd = $derived(
51
- !disabled && (maximumItems === undefined || rows.length < maximumItems),
78
+ allowAdd &&
79
+ !disabled &&
80
+ (maximumItems === undefined || rows.length < maximumItems),
52
81
  );
53
82
  let canRemove = $derived(
54
- !disabled && (minimumItems === undefined || rows.length > minimumItems),
83
+ allowRemove &&
84
+ !disabled &&
85
+ (minimumItems === undefined || rows.length > minimumItems),
55
86
  );
56
87
  let resolvedAddLabel = $derived(
57
88
  addLabel ?? (mode === "map" ? `Add ${label} entry` : `Add ${label} row`),
@@ -131,6 +162,69 @@
131
162
  .map((entry) => entry.row),
132
163
  );
133
164
  }
165
+
166
+ function moveRow(index: number, offset: -1 | 1) {
167
+ if (mode !== "array" || disabled || !reorderable) return;
168
+ const target = index + offset;
169
+ if (target < 0 || target >= rows.length) return;
170
+ const next = rows.map((entry) => entry.row);
171
+ [next[index], next[target]] = [next[target]!, next[index]!];
172
+ emitArray(next);
173
+ }
174
+
175
+ function actionVisible(
176
+ action: WorkspaceObjectRowAction,
177
+ row: Record<string, unknown>,
178
+ ): boolean {
179
+ return typeof action.hidden === "function"
180
+ ? !action.hidden(row)
181
+ : action.hidden !== true;
182
+ }
183
+
184
+ function actionDisabled(
185
+ action: WorkspaceObjectRowAction,
186
+ row: Record<string, unknown>,
187
+ key: string,
188
+ ): boolean {
189
+ stateRevision;
190
+ const configured =
191
+ typeof action.disabled === "function"
192
+ ? action.disabled(row)
193
+ : action.disabled === true;
194
+ return (
195
+ disabled ||
196
+ configured ||
197
+ busyActions[`${key}:${action.id}`] === true ||
198
+ action.busy?.(row) === true
199
+ );
200
+ }
201
+
202
+ async function runRowAction(
203
+ action: WorkspaceObjectRowAction,
204
+ row: Record<string, unknown>,
205
+ index: number,
206
+ key: string,
207
+ ): Promise<void> {
208
+ if (actionDisabled(action, row, key)) return;
209
+ if (action.confirm && !(await action.confirm(row))) return;
210
+ const actionKey = `${key}:${action.id}`;
211
+ delete actionResults[actionKey];
212
+ busyActions[actionKey] = true;
213
+ try {
214
+ const result = await action.run(row, index);
215
+ if (result) actionResults[actionKey] = result;
216
+ } catch (caught) {
217
+ actionResults[actionKey] = {
218
+ tone: "error",
219
+ message:
220
+ caught instanceof Error
221
+ ? caught.message
222
+ : "Unable to run this action",
223
+ };
224
+ } finally {
225
+ busyActions[actionKey] = false;
226
+ }
227
+ }
134
228
  </script>
135
229
 
136
230
  <div class="ui-workspace-setting-object-collection">
@@ -140,14 +234,14 @@
140
234
  {#if mode === "map"}
141
235
  <Table.Head>Name</Table.Head>
142
236
  {/if}
143
- {#each properties as property (property.id)}
237
+ {#each visibleProperties as property (property.id)}
144
238
  <Table.Head>{property.title}</Table.Head>
145
239
  {/each}
146
240
  <Table.Head>Actions</Table.Head>
147
241
  </Table.Row>
148
242
  </Table.Header>
149
243
  <Table.Body>
150
- {#each rows as entry, index (mode === "map" ? entry.key : index)}
244
+ {#each rows as entry, index (entry.key)}
151
245
  <Table.Row>
152
246
  {#if mode === "map"}
153
247
  <Table.Cell>
@@ -160,19 +254,40 @@
160
254
  />
161
255
  </Table.Cell>
162
256
  {/if}
163
- {#each properties as property (property.id)}
257
+ {#each visibleProperties as property (property.id)}
164
258
  <Table.Cell>
165
259
  {#if property.type === "boolean"}
166
260
  <Switch
167
261
  checked={Boolean(entry.row[property.id])}
168
- {disabled}
262
+ disabled={disabled || property.readOnly}
169
263
  aria-label={`${label} ${property.title} row ${index + 1}`}
170
264
  onCheckedChange={(checked) =>
171
265
  updateCell(index, property.id, checked)}
172
266
  />
267
+ {:else if property.presentation === "copyable"}
268
+ <CopyableValue
269
+ value={String(entry.row[property.id] ?? "")}
270
+ label={`${label} ${property.title}`}
271
+ />
272
+ {:else if property.presentation === "code"}
273
+ <code class="ui-workspace-setting-object-code"
274
+ >{String(entry.row[property.id] ?? "")}</code
275
+ >
276
+ {:else if property.presentation === "status"}
277
+ <span class="ui-workspace-setting-object-status"
278
+ >{String(entry.row[property.id] ?? "")}</span
279
+ >
280
+ {:else if property.readOnly}
281
+ <span class="ui-workspace-setting-object-value"
282
+ >{String(entry.row[property.id] ?? "")}</span
283
+ >
173
284
  {:else}
174
285
  <Input
175
- type={property.type === "string" ? "text" : "number"}
286
+ type={property.type === "string"
287
+ ? property.presentation === "url"
288
+ ? "url"
289
+ : "text"
290
+ : "number"}
176
291
  step={property.type === "integer" ? 1 : "any"}
177
292
  aria-label={`${label} ${property.title} row ${index + 1}`}
178
293
  value={String(entry.row[property.id] ?? "")}
@@ -190,23 +305,72 @@
190
305
  </Table.Cell>
191
306
  {/each}
192
307
  <Table.Cell>
193
- <Button
194
- variant="ghost"
195
- size="icon-sm"
196
- aria-label={`Remove ${label} row ${index + 1}`}
197
- disabled={!canRemove}
198
- onclick={() => removeRow(index)}
199
- >
200
- <WorkspaceIcon name="x" />
201
- </Button>
308
+ <div class="ui-workspace-setting-object-actions">
309
+ {#if mode === "array" && reorderable}
310
+ <Button
311
+ variant="ghost"
312
+ size="icon-sm"
313
+ aria-label={`Move ${label} row ${index + 1} up`}
314
+ disabled={disabled || index === 0}
315
+ onclick={() => moveRow(index, -1)}
316
+ >
317
+ <WorkspaceIcon name="arrow-up" />
318
+ </Button>
319
+ <Button
320
+ variant="ghost"
321
+ size="icon-sm"
322
+ aria-label={`Move ${label} row ${index + 1} down`}
323
+ disabled={disabled || index === rows.length - 1}
324
+ onclick={() => moveRow(index, 1)}
325
+ >
326
+ <WorkspaceIcon name="arrow-down" />
327
+ </Button>
328
+ {/if}
329
+ {#each rowActions.filter( (action) => actionVisible(action, entry.row), ) as action (action.id)}
330
+ <Button
331
+ variant={action.variant ?? "ghost"}
332
+ size={action.icon ? "icon-sm" : "sm"}
333
+ aria-label={action.label}
334
+ title={action.label}
335
+ disabled={actionDisabled(action, entry.row, entry.key)}
336
+ onclick={() =>
337
+ void runRowAction(action, entry.row, index, entry.key)}
338
+ >
339
+ {#if action.icon}<WorkspaceIcon name={action.icon} />{/if}
340
+ {#if !action.icon}{action.label}{/if}
341
+ </Button>
342
+ {@const result = actionResults[`${entry.key}:${action.id}`]}
343
+ {#if result}
344
+ <span
345
+ class="ui-workspace-setting-object-action-result"
346
+ data-tone={result.tone}
347
+ role={result.tone === "error" ? "alert" : "status"}
348
+ >{result.message}</span
349
+ >
350
+ {/if}
351
+ {/each}
352
+ {#if !disabled && allowRemove}
353
+ <Button
354
+ variant="ghost"
355
+ size="icon-sm"
356
+ aria-label={`Remove ${label} row ${index + 1}`}
357
+ disabled={!canRemove}
358
+ onclick={() => removeRow(index)}
359
+ >
360
+ <WorkspaceIcon name="x" />
361
+ </Button>
362
+ {/if}
363
+ </div>
202
364
  </Table.Cell>
203
365
  </Table.Row>
204
366
  {/each}
205
367
  </Table.Body>
206
368
  </Table.Root>
207
- <WorkspaceSettingAddButton
208
- label={resolvedAddLabel}
209
- disabled={!canAdd}
210
- onclick={addRow}
211
- />
369
+ {#if !disabled && allowAdd}
370
+ <WorkspaceSettingAddButton
371
+ label={resolvedAddLabel}
372
+ disabled={!canAdd}
373
+ onclick={addRow}
374
+ />
375
+ {/if}
212
376
  </div>
@@ -87,34 +87,34 @@
87
87
  onValueChange={onValueChange as ((next: string[]) => void) | undefined}
88
88
  />
89
89
  {:else}
90
- <SelectRoot
91
- {...rest}
92
- type="single"
93
- {disabled}
94
- bind:value={selectedValue}
95
- onValueChange={onValueChange as ((next: string) => void) | undefined}
96
- >
97
- <Select.Trigger
98
- {id}
99
- role="combobox"
100
- aria-label={ariaLabel}
101
- aria-controls={optionsId}
102
- >
103
- <span class="ui-workspace-setting-select__value">{triggerContent}</span>
104
- </Select.Trigger>
105
- <Select.Content
106
- id={optionsId}
107
- aria-label={ariaLabel ? `${ariaLabel} options` : undefined}
90
+ <SelectRoot
91
+ {...rest}
92
+ type="single"
93
+ {disabled}
94
+ bind:value={selectedValue}
95
+ onValueChange={onValueChange as ((next: string) => void) | undefined}
108
96
  >
109
- <Select.Group>
110
- {#each items as item (item.value)}
111
- <Select.Item
112
- value={item.value}
113
- label={item.label}
114
- disabled={item.disabled}
115
- />
116
- {/each}
117
- </Select.Group>
118
- </Select.Content>
119
- </SelectRoot>
97
+ <Select.Trigger
98
+ {id}
99
+ role="combobox"
100
+ aria-label={ariaLabel}
101
+ aria-controls={optionsId}
102
+ >
103
+ <span class="ui-workspace-setting-select__value">{triggerContent}</span>
104
+ </Select.Trigger>
105
+ <Select.Content
106
+ id={optionsId}
107
+ aria-label={ariaLabel ? `${ariaLabel} options` : undefined}
108
+ >
109
+ <Select.Group>
110
+ {#each items as item (item.value)}
111
+ <Select.Item
112
+ value={item.value}
113
+ label={item.label}
114
+ disabled={item.disabled}
115
+ />
116
+ {/each}
117
+ </Select.Group>
118
+ </Select.Content>
119
+ </SelectRoot>
120
120
  {/if}
@@ -1,4 +1,5 @@
1
1
  .ui-workspace-settings {
2
+ position: relative;
2
3
  display: flex;
3
4
  width: 100%;
4
5
  height: 100%;
@@ -8,6 +9,12 @@
8
9
  color: var(--ui-workspace-foreground);
9
10
  background: var(--ui-workspace-background);
10
11
  font-size: 1rem;
12
+ container-name: workspace-settings;
13
+ container-type: inline-size;
14
+ }
15
+
16
+ .ui-workspace-settings__mobile-toolbar {
17
+ display: none;
11
18
  }
12
19
 
13
20
  .ui-workspace-settings__sidebar {
@@ -667,6 +674,138 @@
667
674
  text-align: right;
668
675
  }
669
676
 
677
+ .ui-workspace-setting-object-actions {
678
+ display: inline-flex;
679
+ align-items: center;
680
+ justify-content: flex-end;
681
+ gap: 0.2rem;
682
+ }
683
+
684
+ .ui-workspace-setting-object-code,
685
+ .ui-workspace-setting-object-value,
686
+ .ui-workspace-setting-object-status {
687
+ display: inline-block;
688
+ max-width: 20rem;
689
+ color: var(--ui-workspace-foreground);
690
+ font-size: 0.8125rem;
691
+ overflow-wrap: anywhere;
692
+ }
693
+
694
+ .ui-workspace-setting-object-code {
695
+ font-family: var(--ui-font-mono, monospace);
696
+ }
697
+
698
+ .ui-workspace-setting-object-status {
699
+ border-radius: 999px;
700
+ padding: 0.2rem 0.5rem;
701
+ color: var(--ui-workspace-muted-foreground);
702
+ background: var(--ui-workspace-muted);
703
+ font-size: 0.75rem;
704
+ font-weight: 600;
705
+ }
706
+
707
+ .ui-workspace-setting-output {
708
+ display: inline-flex;
709
+ min-height: var(--ui-workspace-settings-control-height);
710
+ max-width: 24rem;
711
+ align-items: center;
712
+ color: var(--ui-workspace-foreground);
713
+ font-size: 0.8125rem;
714
+ overflow-wrap: anywhere;
715
+ }
716
+
717
+ .ui-workspace-setting-output[data-presentation="code"] code {
718
+ font-family: var(--ui-font-mono, monospace);
719
+ }
720
+
721
+ .ui-workspace-setting-output[data-presentation="status"] {
722
+ min-height: 1.65rem;
723
+ border-radius: 999px;
724
+ padding: 0.2rem 0.55rem;
725
+ color: var(--ui-workspace-muted-foreground);
726
+ background: var(--ui-workspace-muted);
727
+ font-size: 0.75rem;
728
+ font-weight: 600;
729
+ }
730
+
731
+ .ui-workspace-setting-action-result,
732
+ .ui-workspace-setting-object-action-result {
733
+ margin: 0;
734
+ color: var(--ui-workspace-muted-foreground);
735
+ font-size: 0.75rem;
736
+ }
737
+
738
+ .ui-workspace-setting-action-result[data-tone="success"],
739
+ .ui-workspace-setting-object-action-result[data-tone="success"] {
740
+ color: var(--ui-workspace-success, #15803d);
741
+ }
742
+
743
+ .ui-workspace-setting-action-result[data-tone="warning"],
744
+ .ui-workspace-setting-object-action-result[data-tone="warning"] {
745
+ color: var(--ui-workspace-warning, #a16207);
746
+ }
747
+
748
+ .ui-workspace-setting-action-result[data-tone="error"],
749
+ .ui-workspace-setting-object-action-result[data-tone="error"] {
750
+ color: var(--ui-workspace-destructive, #b91c1c);
751
+ }
752
+
753
+ button.ui-workspace-copyable-value[data-ui-component="button"] {
754
+ --ui-button-padding-inline: 0.4rem;
755
+ min-width: 0;
756
+ max-width: 100%;
757
+ height: 1.65rem;
758
+ gap: 0.35rem;
759
+ border: 1px solid transparent;
760
+ border-radius: var(--ui-workspace-settings-control-radius);
761
+ color: inherit;
762
+ background: color-mix(in srgb, currentcolor 7%, transparent);
763
+ font-weight: 500;
764
+ transition:
765
+ background-color 120ms ease,
766
+ border-color 120ms ease,
767
+ box-shadow 120ms ease;
768
+ }
769
+
770
+ button.ui-workspace-copyable-value[data-ui-component="button"] code {
771
+ min-width: 0;
772
+ overflow: hidden;
773
+ font: 500 0.75rem/1 var(--ui-font-mono, monospace);
774
+ text-overflow: ellipsis;
775
+ white-space: nowrap;
776
+ }
777
+
778
+ button.ui-workspace-copyable-value[data-ui-component="button"]
779
+ [data-ui-component="workspace-icon"] {
780
+ width: 0.75rem;
781
+ height: 0.75rem;
782
+ flex: 0 0 auto;
783
+ }
784
+
785
+ button.ui-workspace-copyable-value[data-copy-failed="true"] {
786
+ color: var(--ui-workspace-destructive);
787
+ }
788
+
789
+ .ui-workspace-copyable-value__empty {
790
+ color: var(--ui-workspace-muted-foreground);
791
+ font-size: 0.8125rem;
792
+ }
793
+
794
+ @media (hover: hover) {
795
+ button.ui-workspace-copyable-value[data-ui-component="button"]:hover {
796
+ border-color: color-mix(in srgb, currentcolor 20%, transparent);
797
+ color: var(--ui-workspace-panel-action-hover-foreground);
798
+ background: var(--ui-workspace-panel-action-hover-background);
799
+ box-shadow: 0 1px 2px color-mix(in srgb, currentcolor 14%, transparent);
800
+ }
801
+ }
802
+
803
+ button.ui-workspace-copyable-value[data-ui-component="button"]:focus-visible {
804
+ border-color: color-mix(in srgb, currentcolor 28%, transparent);
805
+ color: var(--ui-workspace-panel-action-hover-foreground);
806
+ background: var(--ui-workspace-panel-action-hover-background);
807
+ }
808
+
670
809
  .ui-workspace-setting-unsupported {
671
810
  max-width: 22rem;
672
811
  }
@@ -1151,11 +1290,39 @@
1151
1290
  height: 1rem;
1152
1291
  }
1153
1292
 
1154
- @media (max-width: 44rem) {
1293
+ @container workspace-settings (max-width: 44rem) {
1155
1294
  .ui-workspace-settings__sidebar {
1156
- width: 10rem;
1157
- min-width: 8rem;
1158
- flex-basis: 10rem;
1295
+ display: none;
1296
+ }
1297
+
1298
+ .ui-workspace-settings__mobile-toolbar {
1299
+ position: absolute;
1300
+ z-index: 2;
1301
+ inset: 0 0 auto;
1302
+ display: flex;
1303
+ min-width: 0;
1304
+ flex: 0 0 auto;
1305
+ align-items: center;
1306
+ border-bottom: 1px solid var(--ui-workspace-border);
1307
+ padding: 0.375rem 0.5rem;
1308
+ background: var(--ui-workspace-panel);
1309
+ }
1310
+
1311
+ .ui-workspace-settings__content-inner {
1312
+ padding-top: 3.75rem;
1313
+ }
1314
+
1315
+ .ui-workspace-settings__mobile-navigation-trigger {
1316
+ min-width: 0;
1317
+ max-width: 100%;
1318
+ justify-content: flex-start;
1319
+ color: var(--ui-workspace-foreground);
1320
+ }
1321
+
1322
+ .ui-workspace-settings__mobile-navigation-trigger span {
1323
+ overflow: hidden;
1324
+ text-overflow: ellipsis;
1325
+ white-space: nowrap;
1159
1326
  }
1160
1327
 
1161
1328
  .ui-workspace-settings__content-inner {
@@ -1188,3 +1355,22 @@
1188
1355
  flex-wrap: wrap;
1189
1356
  }
1190
1357
  }
1358
+
1359
+ .ui-workspace-settings__mobile-sheet {
1360
+ width: min(20rem, calc(100vw - 2rem));
1361
+ max-width: calc(100vw - 2rem);
1362
+ min-height: 0;
1363
+ gap: 0;
1364
+ overflow: hidden;
1365
+ padding: 0;
1366
+ color: var(--ui-workspace-foreground);
1367
+ background: var(--ui-workspace-panel);
1368
+ }
1369
+
1370
+ .ui-workspace-settings__mobile-sheet-body {
1371
+ display: flex;
1372
+ min-height: 0;
1373
+ flex: 1 1 auto;
1374
+ flex-direction: column;
1375
+ padding-top: 2.75rem;
1376
+ }
@@ -2,7 +2,7 @@
2
2
  import { Input } from "@lapismd/design-core/shadcn/input";
3
3
  import type { WorkspaceCustomSettingProps } from "./types.js";
4
4
 
5
- let { id, value, disabled, update }: WorkspaceCustomSettingProps = $props();
5
+ let { id, value, disabled, setValue }: WorkspaceCustomSettingProps = $props();
6
6
  </script>
7
7
 
8
8
  <label class="ui-workspace-settings-story-custom">
@@ -12,6 +12,6 @@
12
12
  type="text"
13
13
  value={String(value ?? "")}
14
14
  {disabled}
15
- oninput={(event) => update(event.currentTarget.value)}
15
+ oninput={(event) => void setValue(event.currentTarget.value)}
16
16
  />
17
17
  </label>
@@ -0,0 +1,13 @@
1
+ <script lang="ts">
2
+ import type { WorkspaceSettingsPageProps } from "./types.js";
3
+
4
+ let { section }: WorkspaceSettingsPageProps = $props();
5
+ </script>
6
+
7
+ <section aria-labelledby="custom-settings-page-title">
8
+ <h1 id="custom-settings-page-title">{section.title}</h1>
9
+ <article data-setting-id="community.spellcheck">
10
+ <h2>Spellcheck</h2>
11
+ <p>Check prose while editing.</p>
12
+ </article>
13
+ </section>
@@ -1,5 +1,8 @@
1
1
  <script lang="ts">
2
+ import { Button } from "../../shadcn/button/index.js";
3
+ import * as Sheet from "../../shadcn/sheet/index.js";
2
4
  import type { AppShellController } from "../core/app-shell-controller.svelte.js";
5
+ import WorkspaceIcon from "../icon/WorkspaceIcon.svelte";
3
6
  import type { WorkspaceSettingsController } from "./settings-controller.svelte.js";
4
7
  import AppSettingsContent from "./AppSettingsContent.svelte";
5
8
  import AppSettingsNavigation from "./AppSettingsNavigation.svelte";
@@ -13,6 +16,13 @@
13
16
  controller: WorkspaceSettingsController;
14
17
  app?: AppShellController;
15
18
  } = $props();
19
+
20
+ let navigationOpen = $state(false);
21
+ let selectedSection = $derived(
22
+ controller.sections.find(
23
+ (section) => section.id === controller.selectedSectionId,
24
+ ) ?? controller.sections[0],
25
+ );
16
26
  </script>
17
27
 
18
28
  <AppSettingsRoot {controller} {app}>
@@ -24,5 +34,45 @@
24
34
  <AppSettingsSearch />
25
35
  <AppSettingsNavigation />
26
36
  </aside>
37
+ <div
38
+ class="ui-workspace-settings__mobile-toolbar"
39
+ data-ui-part="settings-mobile-toolbar"
40
+ >
41
+ <Sheet.Root bind:open={navigationOpen}>
42
+ <Sheet.Trigger>
43
+ {#snippet child({ props })}
44
+ <Button
45
+ {...props}
46
+ class="ui-workspace-settings__mobile-navigation-trigger"
47
+ variant="ghost"
48
+ size="sm"
49
+ aria-label="Open settings navigation"
50
+ >
51
+ <WorkspaceIcon name="panel-left" data-icon="inline-start" />
52
+ <span>{selectedSection?.title ?? "Settings"}</span>
53
+ </Button>
54
+ {/snippet}
55
+ </Sheet.Trigger>
56
+ <Sheet.Content
57
+ side="left"
58
+ class="ui-workspace-settings__mobile-sheet"
59
+ aria-label="Settings navigation"
60
+ >
61
+ <Sheet.Header class="sr-only">
62
+ <Sheet.Title>Settings navigation</Sheet.Title>
63
+ <Sheet.Description>
64
+ Search settings and choose a settings section.
65
+ </Sheet.Description>
66
+ </Sheet.Header>
67
+ <div
68
+ class="ui-workspace-settings__mobile-sheet-body"
69
+ data-ui-part="settings-mobile-sheet-body"
70
+ >
71
+ <AppSettingsSearch />
72
+ <AppSettingsNavigation onselect={() => (navigationOpen = false)} />
73
+ </div>
74
+ </Sheet.Content>
75
+ </Sheet.Root>
76
+ </div>
27
77
  <AppSettingsContent />
28
78
  </AppSettingsRoot>