@lapis-notes/bases 0.2.1 → 0.3.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/dist/bases-plugin.js +18 -21
  4. package/dist/bases-view/bases.svelte.d.ts +7 -0
  5. package/dist/bases-view/bases.svelte.js +37 -7
  6. package/dist/bases-view/card-view/components/card-placeholder.svelte +7 -3
  7. package/dist/bases-view/card-view/components/card.svelte +23 -15
  8. package/dist/bases-view/cell.svelte +147 -16
  9. package/dist/bases-view/cell.svelte.d.ts +2 -0
  10. package/dist/bases-view/columns.js +3 -2
  11. package/dist/bases-view/components/bases-embed.svelte +28 -0
  12. package/dist/bases-view/components/bases-embed.svelte.d.ts +12 -0
  13. package/dist/bases-view/components/bases-error.svelte +26 -0
  14. package/dist/bases-view/components/bases-error.svelte.d.ts +6 -0
  15. package/dist/bases-view/components/datetime.svelte +3 -0
  16. package/dist/bases-view/components/datetime.svelte.d.ts +1 -0
  17. package/dist/bases-view/components/file-preview.svelte +57 -0
  18. package/dist/bases-view/components/file-preview.svelte.d.ts +8 -0
  19. package/dist/bases-view/components/file.svelte +22 -5
  20. package/dist/bases-view/components/property-picker.svelte +147 -0
  21. package/dist/bases-view/components/property-picker.svelte.d.ts +13 -0
  22. package/dist/bases-view/components/sort/sort-root.svelte +107 -88
  23. package/dist/bases-view/components/sort/sort-root.svelte.d.ts +1 -0
  24. package/dist/bases-view/components/views.svelte +96 -42
  25. package/dist/bases-view/document-core.js +30 -0
  26. package/dist/bases-view/filter-parser.d.ts +1 -1
  27. package/dist/bases-view/filter-parser.js +2 -1
  28. package/dist/bases-view/index.js +15 -0
  29. package/dist/bases-view/kanban-view/configuration.d.ts +8 -0
  30. package/dist/bases-view/kanban-view/configuration.js +40 -0
  31. package/dist/bases-view/kanban-view/index.d.ts +55 -0
  32. package/dist/bases-view/kanban-view/index.js +296 -0
  33. package/dist/bases-view/kanban-view/kanban.svelte +284 -0
  34. package/dist/bases-view/kanban-view/kanban.svelte.d.ts +7 -0
  35. package/dist/bases-view/kanban-view/mapping.d.ts +77 -0
  36. package/dist/bases-view/kanban-view/mapping.js +206 -0
  37. package/dist/bases-view/kanban-view/markdown-detail.svelte +123 -0
  38. package/dist/bases-view/kanban-view/markdown-detail.svelte.d.ts +13 -0
  39. package/dist/bases-view/kanban-view/settings.svelte +87 -0
  40. package/dist/bases-view/kanban-view/settings.svelte.d.ts +7 -0
  41. package/dist/bases-view/list-view/list.svelte +84 -84
  42. package/dist/bases-view/map-view/view.svelte +6 -1
  43. package/dist/bases-view/metadata-invalidation-core.d.ts +1 -0
  44. package/dist/bases-view/metadata-invalidation-core.js +3 -2
  45. package/dist/bases-view/models.d.ts +26 -0
  46. package/dist/bases-view/unknown-view/view.svelte +13 -6
  47. package/dist/bases-view/view-header.svelte +20 -6
  48. package/dist/bases-view/view-registration-inventory.d.ts +8 -0
  49. package/dist/bases-view/view-registration-inventory.js +2 -0
  50. package/dist/bases-view/view-settings.svelte +213 -237
  51. package/dist/index.d.ts +1 -1
  52. package/dist/styles.css +495 -74
  53. package/manifest.json +2 -1
  54. package/package.json +3 -2
  55. package/registry.json +85 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Bases changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#66](https://github.com/lapismd/lapis-plugins/pull/66) [`e6263f6`](https://github.com/lapismd/lapis-plugins/commit/e6263f6d8fa4f24de9af42b5b701fbfd55972299) Thanks [@stevejuma](https://github.com/stevejuma)! - Modernise compact List records with on-demand editing, use bounded Shadcn scrolling and body-surface Cards, add shared file-link hover previews and hover/focus underlines, improve unsupported views, and add populated Kanban and Activity registry examples.
8
+
9
+ - [#62](https://github.com/lapismd/lapis-plugins/pull/62) [`fe8e1d7`](https://github.com/lapismd/lapis-plugins/commit/fe8e1d757b51a15118983727880343042f0c07d3) Thanks [@stevejuma](https://github.com/stevejuma)! - Add configurable Markdown Kanban and Activity layouts, inline file editing, property grids, generic stamp badges, column filters and optional anchoring. Derive summaries from Markdown bodies or mapped properties, preserve the host background in embedded editors and start embedded editing with collapsed properties. Keep dated Activity headings sticky with visible column borders. Admit the released Core 0.12 cohort in History so its settings component shares the host controller type.
10
+
11
+ ### Patch Changes
12
+
13
+ - [#68](https://github.com/lapismd/lapis-plugins/pull/68) [`5a54841`](https://github.com/lapismd/lapis-plugins/commit/5a54841a08814a8ead6bc6c55adc01cb174e72a6) Thanks [@stevejuma](https://github.com/stevejuma)! - Give file and fenced Markdown embeds a bounded host, compact Bases toolbar, working Search and Filter controls, and a filename chip on hover or focus. Preserve read-only behaviour, present invalid content in a Shadcn error alert, name read-only dates for assistive technology, and add Kanban/Activity embed examples. Keep Table/List edges flush and table scrollbar tracks on the body surface. Wrap additional List properties within the available width, retain label/value alignment, and keep group counts and full-width separators visible.
14
+
15
+ Require Design Core ^0.12.3 so repeated Kanban and Activity embeds retain independent accessible landmarks and IDs.
16
+
17
+ - [#72](https://github.com/lapismd/lapis-plugins/pull/72) [`bb8229a`](https://github.com/lapismd/lapis-plugins/commit/bb8229abead6278c12f15b4708810120af3387de) Thanks [@stevejuma](https://github.com/stevejuma)! - Inset embedded grouped List headings before the expand chevron while preserving full-width separators and visible counts.
18
+
19
+ Show automatic Table column sizing in the ordinary Embed example while retaining realistic data and narrow-pane scroll acceptance.
20
+
21
+ - [#61](https://github.com/lapismd/lapis-plugins/pull/61) [`9210719`](https://github.com/lapismd/lapis-plugins/commit/921071900dc14f6ffdcd09080a33cf330135955f) Thanks [@lapismd-dependency-bot](https://github.com/apps/lapismd-dependency-bot)! - Refresh first-party Lapis dependencies to the published versions recorded in this pull request.
22
+
3
23
  ## 0.2.1
4
24
 
5
25
  ### Patch Changes
package/README.md CHANGED
@@ -13,7 +13,7 @@ Register the exported plugin class in the application's `PluginProfile`. Import
13
13
  registration so the host owns its lifecycle.
14
14
 
15
15
  The runtime plugin ID is `bases`. A matching signed
16
- `bases-0.2.1.lapis-plugin` archive is attached to the
16
+ `bases-0.3.0.lapis-plugin` archive is attached to the
17
17
  package-scoped GitHub release for manual or registry installation.
18
18
 
19
19
  See the [repository README](https://github.com/lapismd/lapis-plugins#readme) for
@@ -3,7 +3,8 @@ import { MarkdownRenderChild, Plugin, } from "@lapis-notes/api";
3
3
  import { markupEditor } from "@lapis-notes/api/editor/core";
4
4
  import { yaml } from "@codemirror/lang-yaml";
5
5
  import { mount, unmount } from "svelte";
6
- import BasesComponent from "./bases-view/view.svelte";
6
+ import BasesEmbed from "./bases-view/components/bases-embed.svelte";
7
+ import BasesError from "./bases-view/components/bases-error.svelte";
7
8
  import { BasesView, BasesViewType, createBasesViewRegistrations, parseBasesDocument, } from "./bases-view";
8
9
  import manifestSpec from "@lapis-notes/bases/manifest.json";
9
10
  export class BasesPlugin extends Plugin {
@@ -43,13 +44,13 @@ export class BasesPlugin extends Plugin {
43
44
  if (disposed) {
44
45
  return;
45
46
  }
46
- activeHandle = this.mountReadOnlyBases(containerEl, content);
47
+ activeHandle = this.mountReadOnlyBases(containerEl, content, file.name);
47
48
  })
48
49
  .catch((error) => {
49
50
  if (disposed) {
50
51
  return;
51
52
  }
52
- this.renderError(containerEl, error instanceof Error
53
+ activeHandle = this.renderError(containerEl, error instanceof Error
53
54
  ? `Unable to render this embedded base: ${error.message}`
54
55
  : `Unable to render this embedded base: ${String(error)}`);
55
56
  });
@@ -73,31 +74,24 @@ export class BasesPlugin extends Plugin {
73
74
  ctx.addChild(child);
74
75
  });
75
76
  }
76
- mountReadOnlyBases(containerEl, content) {
77
+ mountReadOnlyBases(containerEl, content, fileName) {
77
78
  containerEl.replaceChildren();
78
79
  let document;
79
80
  try {
80
81
  document = parseBasesDocument(content);
81
82
  }
82
83
  catch (error) {
83
- this.renderError(containerEl, error instanceof Error
84
+ return this.renderError(containerEl, error instanceof Error
84
85
  ? `Unable to render this base: ${error.message}`
85
86
  : `Unable to render this base: ${String(error)}`);
86
- return {
87
- destroy: () => {
88
- containerEl.replaceChildren();
89
- },
90
- };
91
87
  }
92
- const component = mount(BasesComponent, {
88
+ const component = mount(BasesEmbed, {
93
89
  target: containerEl,
94
90
  props: {
95
91
  app: this.app,
96
92
  document,
97
93
  registrations: createBasesViewRegistrations(this.app),
98
- onChange: () => { },
99
- readOnly: true,
100
- showHeader: false,
94
+ fileName,
101
95
  },
102
96
  });
103
97
  return {
@@ -109,13 +103,16 @@ export class BasesPlugin extends Plugin {
109
103
  }
110
104
  renderError(containerEl, message) {
111
105
  containerEl.replaceChildren();
112
- const wrapper = document.createElement("div");
113
- wrapper.className = "bases-view-error";
114
- const messageEl = document.createElement("p");
115
- messageEl.className = "bases-view-error__message";
116
- messageEl.textContent = message;
117
- wrapper.appendChild(messageEl);
118
- containerEl.appendChild(wrapper);
106
+ const component = mount(BasesError, {
107
+ target: containerEl,
108
+ props: { message },
109
+ });
110
+ return {
111
+ destroy: () => {
112
+ void unmount(component);
113
+ containerEl.replaceChildren();
114
+ },
115
+ };
119
116
  }
120
117
  }
121
118
  export default BasesPlugin;
@@ -415,6 +415,7 @@ export declare class QueryController extends Component {
415
415
  selectedView: BasesViewBase;
416
416
  private prevDoc;
417
417
  private lastViewResults;
418
+ get requiredProperties(): BasesPropertyId[];
418
419
  query: string;
419
420
  metadataDependencies: import("./metadata-invalidation-core").MetadataDependencySet;
420
421
  reloadSignature: string;
@@ -426,6 +427,8 @@ export declare class QueryController extends Component {
426
427
  private syncViewData;
427
428
  get viewConfig(): BasesViewRegistration;
428
429
  getRecords(qs: string): [Array<Record<string, unknown>>, Record<string, DType>];
430
+ /** Apply a lane predicate to the already selected result without changing global filters. */
431
+ filterEntries(entries: BasesEntry[], filter: import("./models").Filters): BasesEntry[];
429
432
  getAllColumns(): ColumnDefinition[];
430
433
  isVisible(id: BasesPropertyId): boolean;
431
434
  toggleVisibility(id: BasesPropertyId): void;
@@ -531,6 +534,10 @@ export interface BasesViewRegistration {
531
534
  * @public
532
535
  */
533
536
  options?: (config: SharedBasesViewConfig) => BasesAllOptions[];
537
+ /** Hidden presentation fields that this layout needs in its result projection. */
538
+ requiredProperties?: (view: BasesViewBase, columns: readonly import("./columns").ColumnDefinition[]) => BasesPropertyId[];
539
+ /** Additional layout-owned predicates whose metadata dependencies must stay live. */
540
+ columnFilters?: (view: BasesViewBase) => import("./models").Filters[];
534
541
  }
535
542
  /**
536
543
  * A group of BasesEntry objects for a given value of the groupBy key. If there
@@ -8,7 +8,7 @@ import { generateQuery } from "./filter-parser";
8
8
  import { isEqual } from "lodash-es";
9
9
  import { groupEntries } from "./query-result-core";
10
10
  import { filterEntriesBySearch } from "./search-core";
11
- import { collectMetadataDependencies, } from "./metadata-invalidation-core";
11
+ import { collectMetadataDependencies } from "./metadata-invalidation-core";
12
12
  import { appDatabaseRowsToVaultRecords, buildBasesAppDatabaseQuery, queryBasesAppDatabaseRows, } from "./app-database-query-source";
13
13
  import { mount, unmount } from "svelte";
14
14
  export { BasesViewConfig } from "@lapis-notes/api";
@@ -545,12 +545,23 @@ export class QueryController extends Component {
545
545
  #view = $state();
546
546
  prevDoc = "";
547
547
  lastViewResults = new WeakMap();
548
- query = $derived(generateQuery(this.selectedView, this.formulas, this.doc.filters));
548
+ get requiredProperties() {
549
+ return (this.views
550
+ .get(this.selectedView?.type)
551
+ ?.requiredProperties?.(this.selectedView, this.getAllColumns()) ?? []);
552
+ }
553
+ query = $derived(generateQuery(this.selectedView, this.formulas, this.doc.filters, this.requiredProperties));
549
554
  metadataDependencies = $derived.by(() => {
550
555
  return collectMetadataDependencies({
551
556
  documentFilter: this.doc.filters,
552
557
  viewFilter: this.selectedView?.filter,
553
- order: (this.selectedView?.order ?? []),
558
+ order: [
559
+ ...(this.selectedView?.order ?? []),
560
+ ...this.requiredProperties,
561
+ ],
562
+ additionalFilters: this.views
563
+ .get(this.selectedView?.type)
564
+ ?.columnFilters?.(this.selectedView) ?? [],
554
565
  sort: this.selectedView?.sort ?? [],
555
566
  groupByProperty: this.selectedView?.groupBy?.property ?? null,
556
567
  imageProperty: typeof this.selectedView?.image === "string"
@@ -572,8 +583,10 @@ export class QueryController extends Component {
572
583
  {},
573
584
  ]);
574
585
  queryResult = $derived.by(() => {
575
- const visibleProperties = (this.selectedView?.order ??
576
- []);
586
+ const configuredProperties = this.selectedView?.order ?? [];
587
+ const visibleProperties = (this.selectedView?.type === "list" && configuredProperties.length === 0
588
+ ? ["file.name"]
589
+ : configuredProperties);
577
590
  const entries = filterEntriesBySearch(convertEntries(this.dataset), visibleProperties, this.searchQuery);
578
591
  return new BasesQueryResult(entries, this.getAllColumns().map((it) => it.id), this.selectedView?.groupBy?.property);
579
592
  });
@@ -617,7 +630,7 @@ export class QueryController extends Component {
617
630
  void this.reload();
618
631
  });
619
632
  $effect(() => {
620
- this.dataset = this.getRecords(this.query);
633
+ this.dataset = this.data.length ? this.getRecords(this.query) : [[], {}];
621
634
  });
622
635
  this.registerEvent(this.app.metadataCache.on("index-changed", (change) => {
623
636
  if (change.reset || change.domains.includes("metadata"))
@@ -664,6 +677,21 @@ export class QueryController extends Component {
664
677
  return [[], types];
665
678
  }
666
679
  }
680
+ /** Apply a lane predicate to the already selected result without changing global filters. */
681
+ filterEntries(entries, filter) {
682
+ const query = generateQuery({
683
+ ...this.selectedView,
684
+ order: [],
685
+ sort: [],
686
+ groupBy: undefined,
687
+ limit: 0,
688
+ filter,
689
+ }, this.formulas);
690
+ const [columns, rows] = this.db.execute(query);
691
+ const position = columns.findIndex((column) => column.name.description === "$rowId");
692
+ const ids = new Set(rows.map((row) => row[position].id));
693
+ return entries.filter((entry) => ids.has(entry.id));
694
+ }
667
695
  getAllColumns() {
668
696
  return Object.values(this.columns);
669
697
  }
@@ -713,7 +741,9 @@ export class QueryController extends Component {
713
741
  for (let offset = 0; offset < records.length; offset += 400) {
714
742
  const links = await this.app.metadataCache.queryLinks({
715
743
  direction: "incoming",
716
- paths: records.slice(offset, offset + 400).map((record) => record.id),
744
+ paths: records
745
+ .slice(offset, offset + 400)
746
+ .map((record) => record.id),
717
747
  resolution: "resolved",
718
748
  limit: 100_000,
719
749
  });
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import * as ScrollArea from "@lapismd/design-core/shadcn/scroll-area";
2
3
  import { Skeleton } from "@lapismd/design-core/shadcn/skeleton";
3
4
  import type { BasesView } from "../../bases.svelte";
4
5
  import { toNumber } from "..";
@@ -27,16 +28,17 @@
27
28
  </script>
28
29
 
29
30
  <div
30
- class="bases-card-grid bases-style-h-full-668b21 bases-style-w-full-6da6a3 bases-style-flex-grow-95a3df bases-style-overflow-auto-73fc3f bases-style-pb-16-db15dd bases-style-pl-5-151215"
31
- style={`scrollbar-gutter: stable; transform: translate3d(0,0,0);`}
31
+ class="bases-card-grid bases-record-view"
32
32
  >
33
+ <ScrollArea.Root class="bases-record-view__scroll" viewportProps={{ "aria-label": "Loading cards" }}>
34
+ <div class="bases-card-grid__content">
33
35
  <div class="bases-style-flex-60fbb7 bases-style-w-full-6da6a3 bases-style-items-center-3960ff bases-style-justify-center-86843c">
34
36
  <div class="loader"></div>
35
37
  </div>
36
38
  <div class="bases-style-flex-60fbb7 bases-style-flex-wrap-1eb5c6 bases-style-gap-2-77a2a2">
37
39
  {#each queryResults.data as record}
38
40
  <div
39
- style={`width: ${cardSize}}px; height: ${cardHeight}px;`}
41
+ style={`width: ${cardSize}px; height: ${cardHeight}px;`}
40
42
  class="bases-card-placeholder bases-style-w-full-6da6a3 bases-style-overflow-hidden-2cd02d bases-style-rounded-lg-5f22e6 border bases-style-text-sm-fc7473 bases-style-whitespace-nowrap-e82ae8"
41
43
  >
42
44
  <div class="bases-style-h-full-668b21 bases-style-w-full-6da6a3">
@@ -65,4 +67,6 @@
65
67
  </div>
66
68
  {/each}
67
69
  </div>
70
+ </div>
71
+ </ScrollArea.Root>
68
72
  </div>
@@ -6,7 +6,7 @@
6
6
  import throttle from "lodash-es/throttle";
7
7
  import { createVirtualizer, type MetadataType } from "@lapis-notes/api";
8
8
  import { toNumber } from "..";
9
- import { ScrollArea } from "@lapismd/design-core/shadcn/scroll-area";
9
+ import * as ScrollArea from "@lapismd/design-core/shadcn/scroll-area";
10
10
  import Cell from "../../cell.svelte";
11
11
  import SummaryStrip from "../../components/summary-strip.svelte";
12
12
  import { getMetadataTypeInfo } from "../../metadata-type-info";
@@ -58,7 +58,7 @@
58
58
  });
59
59
 
60
60
  let containerSize = useResizeObserver();
61
- let containerEl: HTMLDivElement = $state()!;
61
+ let containerEl = $state<HTMLElement | null>(null);
62
62
  let cardHeight = $derived.by(() => {
63
63
  return view.config.getOrder().length * 56 + 17 + imageSize + 28;
64
64
  });
@@ -97,7 +97,7 @@
97
97
 
98
98
  const handleResize = throttle(() => {
99
99
  const size = Math.floor(containerSize.size.width / (cardSize + gap.x));
100
- columns = size;
100
+ columns = Math.max(1, size);
101
101
  }, 200);
102
102
 
103
103
  $effect(() => {
@@ -156,7 +156,7 @@
156
156
  </script>
157
157
 
158
158
  {#snippet cardBody(row: BasesEntry)}
159
- <ScrollArea class="bases-style-h-full-668b21 bases-style-w-full-6da6a3">
159
+ <div class="bases-card__body">
160
160
  {#if imageSize}
161
161
  <CardImage
162
162
  app={view.controller.app}
@@ -189,28 +189,34 @@
189
189
  </div>
190
190
  </div>
191
191
  {/each}
192
- </ScrollArea>
192
+ </div>
193
193
  {/snippet}
194
194
 
195
195
  {#if show}
196
196
  <div
197
- class="bases-card-grid bases-style-h-full-668b21 bases-style-w-full-6da6a3 bases-style-flex-grow-95a3df bases-style-overflow-auto-73fc3f bases-style-pb-16-db15dd bases-style-pl-5-151215"
197
+ class="bases-card-grid bases-record-view"
198
198
  data-layout="cards"
199
199
  data-ui-component="bases-card-view"
200
200
  data-ui-part="viewport"
201
- bind:this={
202
- () => containerEl,
203
- (value) => {
204
- containerEl = value;
205
- containerSize.ref = value;
206
- }
207
- }
208
- style={`scrollbar-gutter: stable; transform: translate3d(0,0,0);`}
209
201
  >
210
202
  <SummaryStrip
211
203
  {view}
212
204
  class="bases-card-grid__summary sticky bases-style-top-0-216740 bases-style-left-0-c78fac bases-style-z-90-3d4b39 bases-style-mb-4-da0198 bases-style-py-3-1b2d54 bases-style-pr-5-752a63 backdrop-blur"
213
205
  />
206
+ <ScrollArea.Root
207
+ class="bases-record-view__scroll"
208
+ orientation="both"
209
+ type="always"
210
+ bind:viewportRef={
211
+ () => containerEl,
212
+ (value) => {
213
+ containerEl = value;
214
+ if (value) containerSize.ref = value;
215
+ }
216
+ }
217
+ viewportProps={{ "aria-label": "Cards" }}
218
+ >
219
+ <div class="bases-card-grid__content">
214
220
  {#if groupBy}
215
221
  <div class="bases-style-space-y-6-b3542e bases-style-pr-5-752a63">
216
222
  {#each groups as group, index (`${index}_${group.key?.toString() ?? "ungrouped"}`)}
@@ -261,7 +267,7 @@
261
267
  {:else}
262
268
  <div
263
269
  class="relative"
264
- style={`height: ${rowVirtualizer.getTotalSize() + cardHeight}px; width: ${columnVirtualizer.getTotalSize()}px;`}
270
+ style={`height: ${rowVirtualizer.getTotalSize()}px; width: ${columnVirtualizer.getTotalSize()}px;`}
265
271
  >
266
272
  {#each rowVirtualizer.getVirtualItems() as virtualRow (virtualRow.key)}
267
273
  {#each columnVirtualizer.getVirtualItems() as virtualColumn (virtualColumn.key)}
@@ -283,6 +289,8 @@
283
289
  {/each}
284
290
  </div>
285
291
  {/if}
292
+ </div>
293
+ </ScrollArea.Root>
286
294
  </div>
287
295
  {:else}
288
296
  <CardPlaceholder {view} />
@@ -5,9 +5,13 @@
5
5
  import DatetimeWidget from "./components/datetime.svelte";
6
6
  import { cn, TFile, type App, type MetadataType } from "@lapis-notes/api";
7
7
  import { normalizeMetadataValue } from "@lapis-notes/api/metadata-value";
8
+ import { TagList } from "@lapismd/design-core/shadcn/tag-list";
9
+ import { Button } from "@lapismd/design-core/shadcn/button";
10
+ import Pencil from "@lucide/svelte/icons/pencil";
11
+ import { collectMetadataSuggestions } from "./cell-editor-options";
8
12
  import CellEditor from "./cell-editor.svelte";
9
13
  import { set, unset } from "lodash-es";
10
- import { untrack } from "svelte";
14
+ import { tick, untrack } from "svelte";
11
15
  import { Icon } from "./functions";
12
16
  import { Icon as IconComponent } from "@lapis-notes/api/icon";
13
17
  import File from "./components/file.svelte";
@@ -20,6 +24,8 @@
20
24
  name,
21
25
  value: cellValue,
22
26
  readOnly = false,
27
+ editOnActivation = false,
28
+ label = name.startsWith("note.") ? name.slice(5) : name,
23
29
  class: className,
24
30
  meta,
25
31
  ...rest
@@ -30,6 +36,8 @@
30
36
  value: Value | null | unknown;
31
37
  file: TFile;
32
38
  readOnly?: boolean;
39
+ editOnActivation?: boolean;
40
+ label?: string;
33
41
  meta?: unknown;
34
42
  } = $props();
35
43
 
@@ -40,6 +48,80 @@
40
48
  : (cellValue ?? ""),
41
49
  );
42
50
 
51
+ const descriptionId = $props.id();
52
+ let editing = $state(false);
53
+ let cancelled = false;
54
+ let cellElement = $state<HTMLDivElement>();
55
+ let activateButton = $state<HTMLButtonElement>();
56
+ const writable = $derived(
57
+ !readOnly &&
58
+ !(
59
+ name.startsWith("file.") ||
60
+ name.startsWith("formula.") ||
61
+ name === "file"
62
+ ),
63
+ );
64
+
65
+ const tagItems = $derived(
66
+ collectMetadataSuggestions([value], true).map((tag) => ({
67
+ id: tag,
68
+ label: tag.startsWith("#") ? tag : `#${tag}`,
69
+ tone: "accent" as const,
70
+ onClick: writable
71
+ ? () => {
72
+ void activate();
73
+ }
74
+ : undefined,
75
+ })),
76
+ );
77
+
78
+ async function activate() {
79
+ cancelled = false;
80
+ editing = true;
81
+ await tick();
82
+ const input = cellElement?.querySelector<HTMLInputElement>(
83
+ 'input, textarea, [contenteditable="true"]',
84
+ );
85
+ input?.focus();
86
+ if (input?.type === "text") input.select();
87
+ }
88
+
89
+ function cancelDraft(event: KeyboardEvent) {
90
+ if (!editOnActivation || !editing || event.key !== "Escape") return;
91
+ event.preventDefault();
92
+ event.stopPropagation();
93
+ cancelled = true;
94
+ editing = false;
95
+ void tick().then(() => activateButton?.focus());
96
+ }
97
+
98
+ function finishEditing(event: FocusEvent) {
99
+ if (!editOnActivation || !editing) return;
100
+ // Replacing the focused activation button is not an editor dismissal.
101
+ if (
102
+ !(event.target instanceof Element) ||
103
+ !event.target.closest('[data-ui-component="bases-cell-editor"]')
104
+ )
105
+ return;
106
+ const next = event.relatedTarget;
107
+ if (
108
+ next instanceof Element &&
109
+ (cellElement?.contains(next) ||
110
+ next.closest('[role="listbox"], [role="option"]'))
111
+ )
112
+ return;
113
+ // Child blur handlers keep their existing per-type commit semantics.
114
+ queueMicrotask(() => {
115
+ editing = false;
116
+ });
117
+ }
118
+
119
+ function displayValue() {
120
+ if (value === null || value === undefined || value === "") return "—";
121
+ if (typeof value === "boolean") return value ? "Yes" : "No";
122
+ if (Array.isArray(value)) return value.join(", ") || "—";
123
+ return String(value);
124
+ }
43
125
  function focusElement(evt: Event) {
44
126
  const target = evt.target;
45
127
  if (!(target instanceof HTMLElement) || target.closest("button")) return;
@@ -76,6 +158,7 @@
76
158
  }
77
159
 
78
160
  function onValueChange(name: string, value: any) {
161
+ if (editOnActivation && cancelled) return;
79
162
  if (name.startsWith("file.") || name.startsWith("formula.")) return;
80
163
  untrack(() => {
81
164
  if (file) {
@@ -101,21 +184,13 @@
101
184
  }
102
185
  </script>
103
186
 
104
- <div
105
- {...rest}
106
- class={cn("bases-style-h-full-668b21 bases-style-w-full-6da6a3", className)}
107
- onclick={(evt) => focusElement(evt)}
108
- data-ui-component="bases-cell"
109
- data-ui-part="root"
110
- data-type={displayType}
111
- data-property={name}
112
- >
113
- {#if !readOnly && !(name.startsWith("file.") || name.startsWith("formula.") || name === "file")}
114
- <CellEditor {app} {onValueChange} {value} {name} type={displayType} />
115
- {:else if file && name === "file.name"}
187
+ {#snippet renderedValue()}
188
+ {#if file && name === "file.name"}
116
189
  <File {app} {file} />
117
190
  {:else if name === "file.tags"}
118
- <div class="markdown-rendered bases-style-flex-60fbb7 bases-style-h-full-668b21 bases-style-w-full-6da6a3 bases-style-gap-2-77a2a2">
191
+ <div
192
+ class="markdown-rendered bases-style-flex-60fbb7 bases-style-h-full-668b21 bases-style-w-full-6da6a3 bases-style-gap-2-77a2a2"
193
+ >
119
194
  {#each Array.isArray(value) ? value : [] as tag}
120
195
  <Tag name={tag} />
121
196
  {/each}
@@ -123,10 +198,66 @@
123
198
  {:else if value instanceof TFile}
124
199
  <File {app} file={value} />
125
200
  {:else if value instanceof DateTime}
126
- <DatetimeWidget {value} />
201
+ <DatetimeWidget {value} {label} />
127
202
  {:else if value instanceof Icon}
128
203
  <IconComponent name={[value.name, "circle-question-mark"]} />
129
204
  {:else}
130
- <div class="bases-style-h-full-668b21 bases-style-w-full-6da6a3">{value}</div>
205
+ <div
206
+ class={cn(
207
+ "bases-style-h-full-668b21 bases-style-w-full-6da6a3",
208
+ editOnActivation && "bases-cell__display",
209
+ )}
210
+ >
211
+ {editOnActivation ? displayValue() : value}
212
+ </div>
213
+ {/if}
214
+ {/snippet}
215
+
216
+ <div
217
+ {...rest}
218
+ bind:this={cellElement}
219
+ onkeydowncapture={cancelDraft}
220
+ onfocusout={finishEditing}
221
+ class={cn("bases-style-h-full-668b21 bases-style-w-full-6da6a3", className)}
222
+ onclick={(evt) => focusElement(evt)}
223
+ data-ui-component="bases-cell"
224
+ data-ui-part="root"
225
+ data-type={displayType}
226
+ data-property={name}
227
+ >
228
+ {#if writable && (!editOnActivation || editing)}
229
+ <CellEditor {app} {onValueChange} {value} {name} type={displayType} />
230
+ {:else if editOnActivation && (displayType === "tags" || name === "file.tags")}
231
+ <div class="bases-cell__tags">
232
+ <TagList tags={tagItems} {label} class="bases-cell__tag-list" />
233
+ {#if writable}
234
+ <Button
235
+ type="button"
236
+ variant="ghost"
237
+ size="icon-xs"
238
+ aria-label={`Edit ${label}`}
239
+ onclick={activate}><Pencil aria-hidden="true" /></Button
240
+ >
241
+ {/if}
242
+ </div>
243
+ {:else if writable && editOnActivation}
244
+ <button
245
+ bind:this={activateButton}
246
+ type="button"
247
+ class="bases-cell__activate"
248
+ aria-label={`Edit ${label}`}
249
+ aria-describedby={descriptionId}
250
+ title={displayValue()}
251
+ onclick={activate}
252
+ >
253
+ <span class="bases-cell__display"
254
+ >{value instanceof DateTime
255
+ ? value.toLocaleString(DateTime.DATE_MED)
256
+ : displayValue()}</span
257
+ >
258
+ </button>
259
+ <span id={descriptionId} hidden>{displayValue()}</span>
260
+ {:else}
261
+ {@render renderedValue()}
131
262
  {/if}
132
263
  </div>
@@ -9,6 +9,8 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
9
9
  value: Value | null | unknown;
10
10
  file: TFile;
11
11
  readOnly?: boolean;
12
+ editOnActivation?: boolean;
13
+ label?: string;
12
14
  meta?: unknown;
13
15
  };
14
16
  declare const Cell: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -51,9 +51,10 @@ export function columnsFor(rows, properties = {}, formulas, app) {
51
51
  .forEach((key) => {
52
52
  const name = key.replaceAll(/[^a-zA-Z0-9-_]/g, " ");
53
53
  const { type, icon } = getMetadataTypeInfo(key, observedMetadataTypes[key] ?? "unknown", app);
54
+ const id = `note.${key}`;
54
55
  columns.push({
55
- id: key,
56
- displayName: properties[key]?.displayName || name,
56
+ id,
57
+ displayName: properties[id]?.displayName || properties[key]?.displayName || name,
57
58
  icon,
58
59
  type,
59
60
  });
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import type { App } from "@lapis-notes/api";
3
+ import { Badge } from "@lapismd/design-core/shadcn/badge";
4
+ import BasesComponent from "../view.svelte";
5
+ import type { BasesDocument } from "../models";
6
+ import type { BasesViewRegistration } from "../bases.svelte";
7
+
8
+ let {
9
+ app,
10
+ document,
11
+ registrations,
12
+ fileName,
13
+ }: {
14
+ app: App;
15
+ document: BasesDocument;
16
+ registrations: ReadonlyMap<string, BasesViewRegistration>;
17
+ fileName?: string;
18
+ } = $props();
19
+ </script>
20
+
21
+ <div class="bases-embed">
22
+ <BasesComponent {app} {document} {registrations} readOnly showHeader />
23
+ {#if fileName}
24
+ <Badge variant="secondary" class="bases-embed__file-chip">
25
+ <span class="bases-embed__file-name">{fileName}</span>
26
+ </Badge>
27
+ {/if}
28
+ </div>
@@ -0,0 +1,12 @@
1
+ import type { App } from "@lapis-notes/api";
2
+ import type { BasesDocument } from "../models";
3
+ import type { BasesViewRegistration } from "../bases.svelte";
4
+ type $$ComponentProps = {
5
+ app: App;
6
+ document: BasesDocument;
7
+ registrations: ReadonlyMap<string, BasesViewRegistration>;
8
+ fileName?: string;
9
+ };
10
+ declare const BasesEmbed: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type BasesEmbed = ReturnType<typeof BasesEmbed>;
12
+ export default BasesEmbed;