@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.
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/dist/bases-plugin.js +18 -21
- package/dist/bases-view/bases.svelte.d.ts +7 -0
- package/dist/bases-view/bases.svelte.js +37 -7
- package/dist/bases-view/card-view/components/card-placeholder.svelte +7 -3
- package/dist/bases-view/card-view/components/card.svelte +23 -15
- package/dist/bases-view/cell.svelte +147 -16
- package/dist/bases-view/cell.svelte.d.ts +2 -0
- package/dist/bases-view/columns.js +3 -2
- package/dist/bases-view/components/bases-embed.svelte +28 -0
- package/dist/bases-view/components/bases-embed.svelte.d.ts +12 -0
- package/dist/bases-view/components/bases-error.svelte +26 -0
- package/dist/bases-view/components/bases-error.svelte.d.ts +6 -0
- package/dist/bases-view/components/datetime.svelte +3 -0
- package/dist/bases-view/components/datetime.svelte.d.ts +1 -0
- package/dist/bases-view/components/file-preview.svelte +57 -0
- package/dist/bases-view/components/file-preview.svelte.d.ts +8 -0
- package/dist/bases-view/components/file.svelte +22 -5
- package/dist/bases-view/components/property-picker.svelte +147 -0
- package/dist/bases-view/components/property-picker.svelte.d.ts +13 -0
- package/dist/bases-view/components/sort/sort-root.svelte +107 -88
- package/dist/bases-view/components/sort/sort-root.svelte.d.ts +1 -0
- package/dist/bases-view/components/views.svelte +96 -42
- package/dist/bases-view/document-core.js +30 -0
- package/dist/bases-view/filter-parser.d.ts +1 -1
- package/dist/bases-view/filter-parser.js +2 -1
- package/dist/bases-view/index.js +15 -0
- package/dist/bases-view/kanban-view/configuration.d.ts +8 -0
- package/dist/bases-view/kanban-view/configuration.js +40 -0
- package/dist/bases-view/kanban-view/index.d.ts +55 -0
- package/dist/bases-view/kanban-view/index.js +296 -0
- package/dist/bases-view/kanban-view/kanban.svelte +284 -0
- package/dist/bases-view/kanban-view/kanban.svelte.d.ts +7 -0
- package/dist/bases-view/kanban-view/mapping.d.ts +77 -0
- package/dist/bases-view/kanban-view/mapping.js +206 -0
- package/dist/bases-view/kanban-view/markdown-detail.svelte +123 -0
- package/dist/bases-view/kanban-view/markdown-detail.svelte.d.ts +13 -0
- package/dist/bases-view/kanban-view/settings.svelte +87 -0
- package/dist/bases-view/kanban-view/settings.svelte.d.ts +7 -0
- package/dist/bases-view/list-view/list.svelte +84 -84
- package/dist/bases-view/map-view/view.svelte +6 -1
- package/dist/bases-view/metadata-invalidation-core.d.ts +1 -0
- package/dist/bases-view/metadata-invalidation-core.js +3 -2
- package/dist/bases-view/models.d.ts +26 -0
- package/dist/bases-view/unknown-view/view.svelte +13 -6
- package/dist/bases-view/view-header.svelte +20 -6
- package/dist/bases-view/view-registration-inventory.d.ts +8 -0
- package/dist/bases-view/view-registration-inventory.js +2 -0
- package/dist/bases-view/view-settings.svelte +213 -237
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +495 -74
- package/manifest.json +2 -1
- package/package.json +3 -2
- 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.
|
|
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
|
package/dist/bases-plugin.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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-
|
|
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}
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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
|
-
</
|
|
192
|
+
</div>
|
|
193
193
|
{/snippet}
|
|
194
194
|
|
|
195
195
|
{#if show}
|
|
196
196
|
<div
|
|
197
|
-
class="bases-card-grid bases-
|
|
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()
|
|
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
|
-
|
|
105
|
-
{
|
|
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
|
|
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
|
|
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
|
|
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;
|