@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
@@ -0,0 +1,81 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from "svelte";
3
+ import { Button } from "@lapismd/design-core/shadcn/button";
4
+ import WorkspaceIcon from "../icon/WorkspaceIcon.svelte";
5
+ import { shortenCopyableValue } from "./copyable-value.js";
6
+
7
+ let {
8
+ value,
9
+ label = "value",
10
+ emptyLabel = "None",
11
+ leadingCharacters = 8,
12
+ trailingCharacters = 4,
13
+ class: className,
14
+ testId,
15
+ }: {
16
+ value?: string | null;
17
+ label?: string;
18
+ emptyLabel?: string;
19
+ leadingCharacters?: number;
20
+ trailingCharacters?: number;
21
+ class?: string;
22
+ testId?: string;
23
+ } = $props();
24
+
25
+ let copied = $state(false);
26
+ let failed = $state(false);
27
+ let resetTimer: ReturnType<typeof setTimeout> | undefined;
28
+ let displayValue = $derived(
29
+ value
30
+ ? shortenCopyableValue(value, leadingCharacters, trailingCharacters)
31
+ : emptyLabel,
32
+ );
33
+
34
+ onDestroy(() => {
35
+ if (resetTimer) clearTimeout(resetTimer);
36
+ });
37
+
38
+ async function copy(): Promise<void> {
39
+ if (!value) return;
40
+ copied = false;
41
+ failed = false;
42
+ try {
43
+ const clipboard = globalThis.navigator?.clipboard;
44
+ if (!clipboard?.writeText) throw new Error("Clipboard is unavailable");
45
+ await clipboard.writeText(value);
46
+ copied = true;
47
+ } catch {
48
+ failed = true;
49
+ }
50
+ if (resetTimer) clearTimeout(resetTimer);
51
+ resetTimer = setTimeout(() => {
52
+ copied = false;
53
+ failed = false;
54
+ }, 1_500);
55
+ }
56
+ </script>
57
+
58
+ {#if value}
59
+ <Button
60
+ class={["ui-workspace-copyable-value", className].filter(Boolean).join(" ")}
61
+ variant="ghost"
62
+ size="xs"
63
+ aria-label={`Copy full ${label}: ${value}`}
64
+ title={`Copy full ${label}`}
65
+ data-testid={testId}
66
+ data-copied={copied || undefined}
67
+ data-copy-failed={failed || undefined}
68
+ onclick={() => void copy()}
69
+ >
70
+ <code>{displayValue}</code>
71
+ <WorkspaceIcon
72
+ name={copied ? "check" : failed ? "triangle-alert" : "copy"}
73
+ />
74
+ </Button>
75
+ {:else}
76
+ <span
77
+ class={["ui-workspace-copyable-value__empty", className]
78
+ .filter(Boolean)
79
+ .join(" ")}>{displayValue}</span
80
+ >
81
+ {/if}
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import AppSettingsRoot from "./AppSettingsRoot.svelte";
3
+ import AppSettingsSection from "./AppSettingsSection.svelte";
4
+ import type { WorkspaceSettingsController } from "./settings-controller.svelte.js";
5
+ import type { WorkspaceSettingsSection } from "./types.js";
6
+
7
+ let {
8
+ controller,
9
+ section,
10
+ class: className = "",
11
+ }: {
12
+ controller: WorkspaceSettingsController;
13
+ section: WorkspaceSettingsSection;
14
+ class?: string;
15
+ } = $props();
16
+ </script>
17
+
18
+ <AppSettingsRoot {controller} class={className}>
19
+ <AppSettingsSection {section} />
20
+ </AppSettingsRoot>
@@ -21,6 +21,7 @@
21
21
  import WorkspaceSettingObjectTable from "./WorkspaceSettingObjectTable.svelte";
22
22
  import WorkspaceSettingSelect from "./WorkspaceSettingSelect.svelte";
23
23
  import WorkspaceSettingToggleTable from "./WorkspaceSettingToggleTable.svelte";
24
+ import CopyableValue from "./CopyableValue.svelte";
24
25
 
25
26
  let {
26
27
  controller,
@@ -31,7 +32,12 @@
31
32
  } = $props();
32
33
 
33
34
  let value = $derived(controller.get(field.id));
34
- let error = $derived(controller.validationErrors[field.id]);
35
+ let error = $derived(controller.getError(field.id));
36
+ let busy = $derived(controller.isBusy(field.id));
37
+ let actionResult = $derived(controller.getActionResult(field.id));
38
+ let controlDisabled = $derived(
39
+ field.disabled === true || field.readOnly === true,
40
+ );
35
41
  let options = $state<WorkspaceSettingOption[]>([]);
36
42
  let optionRequest = 0;
37
43
  let keyValueOptions = $derived.by(() => {
@@ -100,7 +106,8 @@
100
106
  return field.type === "key-value" ||
101
107
  field.type === "object-array" ||
102
108
  field.type === "object-grid" ||
103
- field.type === "object-map"
109
+ field.type === "object-map" ||
110
+ (field.type === "custom" && field.presentation === "full-width")
104
111
  ? "stacked"
105
112
  : "row";
106
113
  }
@@ -123,7 +130,7 @@
123
130
  const next = { ...current };
124
131
  delete next[currentKey];
125
132
  next[nextKey] = nextValue;
126
- controller.update(field.id, next);
133
+ void controller.set(field.id, next);
127
134
  }
128
135
 
129
136
  function nextKey(field: WorkspaceKeyValueSetting) {
@@ -180,17 +187,17 @@
180
187
  <Switch
181
188
  id={`setting-${field.id}`}
182
189
  checked={value === true}
183
- disabled={field.disabled}
184
- onCheckedChange={(checked) => controller.update(field.id, checked)}
190
+ disabled={controlDisabled}
191
+ onCheckedChange={(checked) => void controller.set(field.id, checked)}
185
192
  />
186
193
  {:else if field.type === "string" && field.presentation === "icon"}
187
194
  <WorkspaceIconPicker
188
195
  id={`setting-${field.id}`}
189
196
  value={String(value ?? "")}
190
- disabled={field.disabled}
197
+ disabled={controlDisabled}
191
198
  placeholder={field.placeholder ?? "Select an icon"}
192
199
  ariaLabel={field.title}
193
- onValueChange={(next) => controller.update(field.id, next)}
200
+ onValueChange={(next) => void controller.set(field.id, next)}
194
201
  />
195
202
  {:else if field.type === "string" && field.presentation === "textarea"}
196
203
  <Textarea
@@ -198,30 +205,30 @@
198
205
  rows={1}
199
206
  value={String(value ?? "")}
200
207
  placeholder={field.placeholder}
201
- disabled={field.disabled}
208
+ disabled={controlDisabled}
202
209
  aria-invalid={Boolean(error)}
203
210
  oninput={(event) =>
204
- controller.update(field.id, event.currentTarget.value)}
211
+ void controller.set(field.id, event.currentTarget.value)}
205
212
  />
206
213
  {:else if field.type === "string" && (field.presentation === "combobox" || field.optionsSource)}
207
214
  <WorkspaceSettingSelect
208
215
  id={`setting-${field.id}`}
209
216
  items={options}
210
217
  value={String(value ?? field.default)}
211
- disabled={field.disabled}
218
+ disabled={controlDisabled}
212
219
  ariaLabel={field.title}
213
220
  placeholder={field.placeholder}
214
- onValueChange={(next: string) => controller.update(field.id, next)}
221
+ onValueChange={(next: string) => void controller.set(field.id, next)}
215
222
  />
216
223
  {:else if field.type === "string" && field.presentation === "password"}
217
224
  <PasswordInput
218
225
  id={`setting-${field.id}`}
219
226
  value={String(value ?? "")}
220
227
  placeholder={field.placeholder}
221
- disabled={field.disabled}
228
+ disabled={controlDisabled}
222
229
  aria-invalid={Boolean(error)}
223
230
  oninput={(event) =>
224
- controller.update(field.id, event.currentTarget.value)}
231
+ void controller.set(field.id, event.currentTarget.value)}
225
232
  />
226
233
  {:else if field.type === "string"}
227
234
  <Input
@@ -239,10 +246,10 @@
239
246
  : "text"}
240
247
  value={String(value ?? "")}
241
248
  placeholder={field.placeholder}
242
- disabled={field.disabled}
249
+ disabled={controlDisabled}
243
250
  aria-invalid={Boolean(error)}
244
251
  oninput={(event) =>
245
- controller.update(field.id, event.currentTarget.value)}
252
+ void controller.set(field.id, event.currentTarget.value)}
246
253
  />
247
254
  {:else if (field.type === "number" || field.type === "integer") && field.minimum !== undefined && field.maximum !== undefined}
248
255
  <div class="ui-workspace-setting-range">
@@ -253,9 +260,10 @@
253
260
  min={field.minimum}
254
261
  max={field.maximum}
255
262
  step={field.step ?? (field.type === "integer" ? 1 : 0.1)}
256
- disabled={field.disabled}
263
+ disabled={controlDisabled}
257
264
  aria-label={field.title}
258
- onValueChange={(next: number) => controller.update(field.id, next)}
265
+ onValueChange={(next: number) =>
266
+ void controller.set(field.id, next)}
259
267
  />
260
268
  <output for={`setting-${field.id}`}
261
269
  >{Number(value ?? field.default)}</output
@@ -269,18 +277,18 @@
269
277
  min={field.minimum}
270
278
  max={field.maximum}
271
279
  step={field.step ?? (field.type === "integer" ? 1 : "any")}
272
- disabled={field.disabled}
280
+ disabled={controlDisabled}
273
281
  onchange={(event) =>
274
- controller.update(field.id, event.currentTarget.valueAsNumber)}
282
+ void controller.set(field.id, event.currentTarget.valueAsNumber)}
275
283
  />
276
284
  {:else if field.type === "enum"}
277
285
  <WorkspaceSettingSelect
278
286
  id={`setting-${field.id}`}
279
287
  items={options}
280
288
  value={String(value ?? field.default)}
281
- disabled={field.disabled}
289
+ disabled={controlDisabled}
282
290
  ariaLabel={field.title}
283
- onValueChange={(next: string) => controller.update(field.id, next)}
291
+ onValueChange={(next: string) => void controller.set(field.id, next)}
284
292
  />
285
293
  {:else if field.type === "multi-enum"}
286
294
  <WorkspaceSettingSelect
@@ -292,10 +300,11 @@
292
300
  (entry): entry is string => typeof entry === "string",
293
301
  )
294
302
  : field.default}
295
- disabled={field.disabled}
303
+ disabled={controlDisabled}
296
304
  ariaLabel={field.title}
297
305
  placeholder="Select options..."
298
- onValueChange={(next: string[]) => controller.update(field.id, next)}
306
+ onValueChange={(next: string[]) =>
307
+ void controller.set(field.id, next)}
299
308
  />
300
309
  {:else if field.type === "list"}
301
310
  <WorkspaceSettingList
@@ -303,19 +312,26 @@
303
312
  label={field.title}
304
313
  itemLabels={field.itemLabels}
305
314
  value={Array.isArray(value) ? value : []}
306
- disabled={field.disabled}
315
+ disabled={controlDisabled}
307
316
  maximumItems={field.maximumItems}
308
- onValueChange={(next) => controller.update(field.id, next)}
317
+ onValueChange={(next) => void controller.set(field.id, next)}
309
318
  />
310
319
  {:else if field.type === "object-array" || field.type === "object-grid"}
311
320
  <WorkspaceSettingObjectTable
312
321
  label={field.title}
313
322
  properties={field.properties}
314
323
  value={value ?? field.default}
315
- disabled={field.disabled}
324
+ disabled={controlDisabled}
316
325
  minimumItems={field.minimumItems}
317
326
  maximumItems={field.maximumItems}
318
- onValueChange={(next) => controller.update(field.id, next)}
327
+ rowKey={field.rowKey}
328
+ reorderable={field.reorderable}
329
+ allowAdd={field.allowAdd}
330
+ allowRemove={field.allowRemove}
331
+ stateRevision={controller.sourceRevision}
332
+ addLabel={field.addLabel}
333
+ rowActions={field.rowActions}
334
+ onValueChange={(next) => void controller.set(field.id, next)}
319
335
  />
320
336
  {:else if field.type === "object-map"}
321
337
  <WorkspaceSettingObjectTable
@@ -323,8 +339,8 @@
323
339
  properties={field.properties}
324
340
  mode="map"
325
341
  value={value ?? field.default}
326
- disabled={field.disabled}
327
- onValueChange={(next) => controller.update(field.id, next)}
342
+ disabled={controlDisabled}
343
+ onValueChange={(next) => void controller.set(field.id, next)}
328
344
  />
329
345
  {:else if field.type === "key-value"}
330
346
  <div class="ui-workspace-setting-key-value">
@@ -370,7 +386,7 @@
370
386
  onclick={() => {
371
387
  const next = { ...keyValue(field) };
372
388
  delete next[key];
373
- controller.update(field.id, next);
389
+ void controller.set(field.id, next);
374
390
  }}
375
391
  >
376
392
  <WorkspaceIcon name="trash-2" />
@@ -383,20 +399,62 @@
383
399
  <WorkspaceSettingAddButton
384
400
  label={field.addLabel ?? "Add entry"}
385
401
  onclick={() =>
386
- controller.update(field.id, {
402
+ void controller.set(field.id, {
387
403
  ...keyValue(field),
388
404
  [nextKey(field)]: keyValueOptions[0]?.value ?? "",
389
405
  })}
390
406
  />
391
407
  </div>
408
+ {:else if field.type === "output"}
409
+ {#if field.presentation === "copyable"}
410
+ <CopyableValue
411
+ value={value == null ? null : String(value)}
412
+ label={field.copyLabel ?? field.title}
413
+ emptyLabel={field.emptyLabel}
414
+ leadingCharacters={field.leadingCharacters}
415
+ trailingCharacters={field.trailingCharacters}
416
+ />
417
+ {:else}
418
+ <output
419
+ id={`setting-${field.id}`}
420
+ class="ui-workspace-setting-output"
421
+ data-presentation={field.presentation ?? "text"}
422
+ >
423
+ {#if field.presentation === "code"}
424
+ <code
425
+ >{value == null
426
+ ? (field.emptyLabel ?? "None")
427
+ : String(value)}</code
428
+ >
429
+ {:else}
430
+ {value == null ? (field.emptyLabel ?? "None") : String(value)}
431
+ {/if}
432
+ </output>
433
+ {/if}
392
434
  {:else if field.type === "custom"}
393
- {@const CustomComponent = field.component}
394
- <CustomComponent
395
- id={field.id}
396
- {value}
397
- disabled={field.disabled}
398
- update={(next) => controller.update(field.id, next)}
399
- />
435
+ {@const CustomComponent = field.adapter
436
+ ? controller.resolveCustomAdapter(field.id, field.adapter)?.component
437
+ : field.component}
438
+ {#if CustomComponent}
439
+ <CustomComponent
440
+ id={field.id}
441
+ {field}
442
+ {value}
443
+ disabled={controlDisabled}
444
+ readOnly={field.readOnly}
445
+ {busy}
446
+ {error}
447
+ setValue={(next) => controller.set(field.id, next)}
448
+ update={(next) => controller.update(field.id, next)}
449
+ />
450
+ {:else}
451
+ <Alert.Root class="ui-workspace-setting-unsupported">
452
+ <Alert.Title>Unavailable custom field</Alert.Title>
453
+ <Alert.Description>
454
+ The adapter for this setting is not registered.
455
+ </Alert.Description>
456
+ </Alert.Root>
457
+ {/if}
400
458
  {:else if field.type === "unsupported"}
401
459
  <Alert.Root class="ui-workspace-setting-unsupported">
402
460
  <Alert.Title>Unsupported setting</Alert.Title>
@@ -410,7 +468,8 @@
410
468
  <Button
411
469
  id={`setting-${field.id}`}
412
470
  variant={field.variant ?? "outline"}
413
- disabled={field.disabled}
471
+ disabled={controller.isDisabled(field.id) || busy}
472
+ aria-busy={busy}
414
473
  onclick={() => controller.runAction(field.id)}
415
474
  >
416
475
  {#if field.icon}<WorkspaceIcon name={field.icon} />{/if}
@@ -418,14 +477,24 @@
418
477
  </Button>
419
478
  {/if}
420
479
 
421
- {#if field.type !== "action"}
480
+ {#if field.type === "action" && actionResult}
481
+ <p
482
+ class="ui-workspace-setting-action-result"
483
+ data-tone={actionResult.tone}
484
+ role={actionResult.tone === "error" ? "alert" : "status"}
485
+ >
486
+ {actionResult.message}
487
+ </p>
488
+ {/if}
489
+
490
+ {#if field.type !== "action" && field.type !== "output" && "default" in field && field.default !== undefined}
422
491
  <Button
423
492
  class="ui-workspace-setting-restore"
424
493
  variant="ghost"
425
494
  size="icon-sm"
426
495
  aria-label={`Restore ${field.title} default`}
427
496
  title="Restore default"
428
- disabled={field.disabled}
497
+ disabled={controlDisabled}
429
498
  onclick={() => controller.restoreDefault(field.id)}
430
499
  >
431
500
  <WorkspaceIcon name="rotate-ccw" />
@@ -85,7 +85,9 @@
85
85
  dataUiComponent="workspace-setting-multiselect"
86
86
  data-ui-part="trigger"
87
87
  >
88
- <span class="ui-workspace-setting-select__value">{triggerContent}</span>
88
+ <span class="ui-workspace-setting-select__value"
89
+ >{triggerContent}</span
90
+ >
89
91
  <span data-ui-part="chevron">
90
92
  <WorkspaceIcon name="chevrons-up-down" />
91
93
  </span>
@@ -100,17 +102,16 @@
100
102
  data-ui-component="workspace-setting-multiselect"
101
103
  data-ui-part="content"
102
104
  >
103
- <CommandView.Root
104
- shouldFilter={false}
105
- label={ariaLabel ?? placeholder}
106
- >
105
+ <CommandView.Root shouldFilter={false} label={ariaLabel ?? placeholder}>
107
106
  <CommandView.Input
108
107
  bind:value={query}
109
108
  placeholder="Search options..."
110
109
  autocomplete="off"
111
110
  spellcheck="false"
112
111
  />
113
- <CommandView.List aria-label={ariaLabel ? `${ariaLabel} options` : "Options"}>
112
+ <CommandView.List
113
+ aria-label={ariaLabel ? `${ariaLabel} options` : "Options"}
114
+ >
114
115
  <CommandView.Empty>No options found.</CommandView.Empty>
115
116
  {#if filtered.length > 0}
116
117
  <CommandView.Group>