@lobb-js/studio 0.29.1 → 0.31.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/dist/actions.d.ts +11 -0
- package/dist/actions.js +16 -0
- package/dist/components/Studio.svelte +1 -10
- package/dist/components/dataTable/dataTable.svelte +20 -2
- package/dist/components/dataTable/fieldCell.svelte +3 -0
- package/dist/components/dataTable/filter.svelte +0 -15
- package/dist/components/dataTable/numberCell.svelte +28 -0
- package/dist/components/dataTable/numberCell.svelte.d.ts +7 -0
- package/dist/components/dataTablePopup/dataTablePopup.svelte +84 -0
- package/dist/components/dataTablePopup/dataTablePopup.svelte.d.ts +17 -0
- package/dist/components/detailView/detailView.svelte +6 -1
- package/dist/components/detailView/fieldInput.svelte +7 -5
- package/dist/components/miniSidebar.svelte +6 -3
- package/dist/components/routes/extensions/extension.svelte +1 -1
- package/dist/components/routes/home.svelte +35 -21
- package/dist/components/ui/input/numberInput.svelte +104 -0
- package/dist/components/ui/input/numberInput.svelte.d.ts +9 -0
- package/dist/extensions/extension.types.d.ts +2 -1
- package/dist/extensions/extensionUtils.js +2 -1
- package/package.json +3 -2
- package/src/lib/actions.ts +28 -0
- package/src/lib/components/Studio.svelte +1 -10
- package/src/lib/components/dataTable/dataTable.svelte +20 -2
- package/src/lib/components/dataTable/fieldCell.svelte +3 -0
- package/src/lib/components/dataTable/filter.svelte +0 -15
- package/src/lib/components/dataTable/numberCell.svelte +28 -0
- package/src/lib/components/dataTablePopup/dataTablePopup.svelte +84 -0
- package/src/lib/components/detailView/detailView.svelte +6 -1
- package/src/lib/components/detailView/fieldInput.svelte +7 -5
- package/src/lib/components/miniSidebar.svelte +6 -3
- package/src/lib/components/routes/extensions/extension.svelte +1 -1
- package/src/lib/components/routes/home.svelte +35 -21
- package/src/lib/components/ui/input/numberInput.svelte +104 -0
- package/src/lib/extensions/extension.types.ts +2 -1
- package/src/lib/extensions/extensionUtils.ts +2 -1
- package/dist/components/breadCrumbs.svelte +0 -61
- package/dist/components/breadCrumbs.svelte.d.ts +0 -3
- package/dist/components/header.svelte +0 -45
- package/dist/components/header.svelte.d.ts +0 -6
- package/src/lib/components/breadCrumbs.svelte +0 -61
- package/src/lib/components/header.svelte +0 -45
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import { ModeWatcher } from "mode-watcher";
|
|
5
5
|
import { createLobb } from "../store.svelte";
|
|
6
6
|
import { setStudioContext } from "../context";
|
|
7
|
-
import Header from "./header.svelte";
|
|
8
7
|
import { LoaderCircle, ServerOff } from "lucide-svelte";
|
|
9
8
|
import MiniSidebar from "./miniSidebar.svelte";
|
|
10
9
|
import * as Tooltip from "./ui/tooltip";
|
|
@@ -107,8 +106,7 @@
|
|
|
107
106
|
style="display: grid; grid-template-columns: {isSmallScreen ? '1fr' : '3.5rem 1fr'};"
|
|
108
107
|
>
|
|
109
108
|
<MiniSidebar />
|
|
110
|
-
<div class="
|
|
111
|
-
<Header />
|
|
109
|
+
<div class="min-h-0 h-screen overflow-hidden">
|
|
112
110
|
{#if page.url.pathname.replace(/\/$/, "") === "/studio"}
|
|
113
111
|
<Home />
|
|
114
112
|
{:else if page.url.pathname.startsWith("/studio/collections")}
|
|
@@ -131,10 +129,3 @@
|
|
|
131
129
|
</main>
|
|
132
130
|
</Tooltip.Provider>
|
|
133
131
|
{/if}
|
|
134
|
-
|
|
135
|
-
<style>
|
|
136
|
-
.second_grid {
|
|
137
|
-
display: grid;
|
|
138
|
-
grid-template-rows: 2.5rem 1fr;
|
|
139
|
-
}
|
|
140
|
-
</style>
|
|
@@ -122,10 +122,28 @@
|
|
|
122
122
|
|
|
123
123
|
let activeTabFilter = $state<any>(undefined);
|
|
124
124
|
|
|
125
|
+
// Canonicalize the incoming filter so values like `{ status: "Open" }`
|
|
126
|
+
// become `{ status: { $eq: "Open" } }`. The Filter UI and the server
|
|
127
|
+
// both expect operator objects, so doing this once at the boundary
|
|
128
|
+
// keeps the rest of the data flow uniform.
|
|
129
|
+
function normalizeFilter(f: Record<string, any> | undefined) {
|
|
130
|
+
const out: Record<string, any> = {};
|
|
131
|
+
for (const [key, value] of Object.entries(f ?? {})) {
|
|
132
|
+
if (key === "$and" || key === "$or") {
|
|
133
|
+
out[key] = value;
|
|
134
|
+
} else if (!_.isPlainObject(value)) {
|
|
135
|
+
out[key] = { $eq: value };
|
|
136
|
+
} else {
|
|
137
|
+
out[key] = value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
125
143
|
const fields = getCollectionParamsFields(ctx, collectionName);
|
|
126
144
|
let params = $state({
|
|
127
145
|
fields: fields,
|
|
128
|
-
filter: { ...filter, ...activeTabFilter },
|
|
146
|
+
filter: { ...normalizeFilter(filter), ...activeTabFilter },
|
|
129
147
|
sort: {},
|
|
130
148
|
limit: "100",
|
|
131
149
|
page: 1,
|
|
@@ -134,7 +152,7 @@
|
|
|
134
152
|
|
|
135
153
|
$effect(() => {
|
|
136
154
|
const tabFilter = activeTabFilter;
|
|
137
|
-
params.filter = { ...filter, ...tabFilter };
|
|
155
|
+
params.filter = { ...normalizeFilter(filter), ...tabFilter };
|
|
138
156
|
});
|
|
139
157
|
|
|
140
158
|
let selectedRecords = $state([]);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { getStudioContext } from "../../context";
|
|
9
9
|
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
10
10
|
import { getExtensionUtils } from "../../extensions/extensionUtils";
|
|
11
|
+
import NumberCell from "./numberCell.svelte";
|
|
11
12
|
|
|
12
13
|
const { ctx, lobb } = getStudioContext();
|
|
13
14
|
|
|
@@ -92,6 +93,8 @@
|
|
|
92
93
|
<div>{date}</div>
|
|
93
94
|
{:else if field.type === "time"}
|
|
94
95
|
<div>{value}</div>
|
|
96
|
+
{:else if field.type === "integer" || field.type === "long" || field.type === "decimal" || field.type === "float"}
|
|
97
|
+
<NumberCell {value} groupDigits={field.ui?.groupDigits ?? false} />
|
|
95
98
|
{:else}
|
|
96
99
|
{value}
|
|
97
100
|
{/if}
|
|
@@ -33,21 +33,6 @@
|
|
|
33
33
|
let firstPopover = $state(false);
|
|
34
34
|
let secondPopover = $state(false);
|
|
35
35
|
|
|
36
|
-
$effect.pre(() => {
|
|
37
|
-
// convert direct values to { $eq: (value) }
|
|
38
|
-
for (let index = 0; index < Object.keys(filter).length; index++) {
|
|
39
|
-
const key = Object.keys(filter)[index];
|
|
40
|
-
const value = filter[key];
|
|
41
|
-
if (key !== "$and" && key !== "$or") {
|
|
42
|
-
if (!_.isPlainObject(value)) {
|
|
43
|
-
filter[key] = {
|
|
44
|
-
$eq: value,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
36
|
function groupAddingHandler(filter: any, key: string) {
|
|
52
37
|
if (key === "$and" || key === "$or") {
|
|
53
38
|
filter[key] = [];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Matches the input mask in numberInput.svelte: space-grouped thousands,
|
|
3
|
+
// dot decimal (ISO 31-0). Up to 20 fractional digits so we don't silently
|
|
4
|
+
// round decimal column values on display.
|
|
5
|
+
const formatter = new Intl.NumberFormat("en-US", {
|
|
6
|
+
useGrouping: true,
|
|
7
|
+
maximumFractionDigits: 20,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
value: any;
|
|
12
|
+
// When false, render the value plainly with no grouping. Default false
|
|
13
|
+
// so dropping this in for any number type is safe; opt into grouping
|
|
14
|
+
// only where it makes sense (quantities/amounts, not identifiers).
|
|
15
|
+
groupDigits?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { value, groupDigits = false }: Props = $props();
|
|
19
|
+
|
|
20
|
+
const formatted = $derived.by(() => {
|
|
21
|
+
if (!groupDigits) return String(value);
|
|
22
|
+
const n = Number(value);
|
|
23
|
+
if (!Number.isFinite(n)) return String(value);
|
|
24
|
+
return formatter.format(n).replaceAll(",", " ");
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<div class="tabular-nums">{formatted}</div>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { X } from "lucide-svelte";
|
|
3
|
+
import { untrack } from "svelte";
|
|
4
|
+
import { fade, scale } from "svelte/transition";
|
|
5
|
+
import { cubicOut } from "svelte/easing";
|
|
6
|
+
import Portal from "svelte-portal";
|
|
7
|
+
import Button from "../ui/button/button.svelte";
|
|
8
|
+
import DataTable from "../dataTable/dataTable.svelte";
|
|
9
|
+
import type { TableProps } from "../dataTable/table.svelte";
|
|
10
|
+
import type { CollectionTab } from "../../store.types";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
collectionName: string;
|
|
14
|
+
filter?: Record<string, any>;
|
|
15
|
+
sort?: Record<string, "asc" | "desc">;
|
|
16
|
+
limit?: number;
|
|
17
|
+
title?: string;
|
|
18
|
+
showHeader?: boolean;
|
|
19
|
+
showFooter?: boolean;
|
|
20
|
+
tableProps?: Partial<TableProps>;
|
|
21
|
+
tabs?: CollectionTab[];
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
collectionName,
|
|
27
|
+
filter,
|
|
28
|
+
sort,
|
|
29
|
+
limit,
|
|
30
|
+
title,
|
|
31
|
+
showHeader = true,
|
|
32
|
+
showFooter = true,
|
|
33
|
+
tableProps,
|
|
34
|
+
tabs,
|
|
35
|
+
onClose,
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
// Read once on mount — sort/limit are fixed for the popup's lifetime,
|
|
39
|
+
// and DataTable only reads searchParams during its initial $state setup
|
|
40
|
+
// so even live updates wouldn't propagate. untrack makes that intent
|
|
41
|
+
// explicit and silences Svelte's "captures initial value" warning.
|
|
42
|
+
const searchParams = untrack(() => {
|
|
43
|
+
const p: Record<string, any> = {};
|
|
44
|
+
if (sort) p.sort = sort;
|
|
45
|
+
if (limit != null) p.limit = String(limit);
|
|
46
|
+
return p;
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<Portal target="body">
|
|
51
|
+
<button
|
|
52
|
+
transition:fade={{ duration: 200 }}
|
|
53
|
+
onclick={() => onClose?.()}
|
|
54
|
+
class="fixed left-0 top-0 z-40 h-screen w-screen bg-black/50 cursor-default"
|
|
55
|
+
aria-label="background used to close the popup"
|
|
56
|
+
></button>
|
|
57
|
+
|
|
58
|
+
<div
|
|
59
|
+
transition:scale={{ duration: 200, easing: cubicOut, start: 0.95 }}
|
|
60
|
+
class="fixed left-1/2 top-1/2 z-40 flex h-[85vh] w-[95vw] max-w-7xl -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border bg-background shadow-2xl"
|
|
61
|
+
>
|
|
62
|
+
<div class="flex h-12 shrink-0 items-center justify-between gap-4 border-b px-4">
|
|
63
|
+
<div class="text-sm font-medium">{title ?? collectionName}</div>
|
|
64
|
+
<Button
|
|
65
|
+
variant="ghost"
|
|
66
|
+
size="icon"
|
|
67
|
+
onclick={() => onClose?.()}
|
|
68
|
+
class="h-8 w-8 rounded-full text-muted-foreground"
|
|
69
|
+
Icon={X}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="min-h-0 flex-1 overflow-auto">
|
|
73
|
+
<DataTable
|
|
74
|
+
{collectionName}
|
|
75
|
+
{filter}
|
|
76
|
+
{searchParams}
|
|
77
|
+
{showHeader}
|
|
78
|
+
{showFooter}
|
|
79
|
+
{tableProps}
|
|
80
|
+
{tabs}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</Portal>
|
|
@@ -20,8 +20,13 @@
|
|
|
20
20
|
}: Props = $props();
|
|
21
21
|
|
|
22
22
|
const { lobb, ctx } = getStudioContext();
|
|
23
|
+
// Singleton collections only ever have one row, and `id` is auto-
|
|
24
|
+
// generated for that row — there's no value to showing it in the form.
|
|
23
25
|
const fieldNames = $derived(
|
|
24
|
-
Object.keys(ctx.meta.collections[collectionName].fields)
|
|
26
|
+
Object.keys(ctx.meta.collections[collectionName].fields).filter(
|
|
27
|
+
(fieldName) =>
|
|
28
|
+
!(ctx.meta.collections[collectionName].singleton && fieldName === "id"),
|
|
29
|
+
),
|
|
25
30
|
);
|
|
26
31
|
</script>
|
|
27
32
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import Button from "../ui/button/button.svelte";
|
|
7
7
|
import FieldCustomInput from "./fieldCustomInput.svelte";
|
|
8
8
|
import Input from "../ui/input/input.svelte";
|
|
9
|
+
import NumberInput from "../ui/input/numberInput.svelte";
|
|
9
10
|
import * as Select from "../ui/select/index";
|
|
10
11
|
import EnumBadge from "../dataTable/enumBadge.svelte";
|
|
11
12
|
import type { EnumOption } from "@lobb-js/core";
|
|
@@ -257,11 +258,12 @@
|
|
|
257
258
|
</Select.Item>
|
|
258
259
|
</Select.Content>
|
|
259
260
|
</Select.Root>
|
|
260
|
-
{:else if field.type === "decimal"}
|
|
261
|
-
|
|
261
|
+
{:else if field.type === "decimal" || field.type === "float" || field.type === "integer" || field.type === "long"}
|
|
262
|
+
{@const isFloat = field.type === "decimal" || field.type === "float"}
|
|
263
|
+
<NumberInput
|
|
262
264
|
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
scale={isFloat ? 20 : 0}
|
|
266
|
+
groupDigits={ui?.groupDigits ?? false}
|
|
265
267
|
class="
|
|
266
268
|
bg-muted-soft text-xs
|
|
267
269
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
@@ -271,7 +273,7 @@
|
|
|
271
273
|
{:else}
|
|
272
274
|
<Input
|
|
273
275
|
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
274
|
-
type="
|
|
276
|
+
type="text"
|
|
275
277
|
class="
|
|
276
278
|
bg-muted-soft text-xs
|
|
277
279
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
@@ -90,12 +90,15 @@
|
|
|
90
90
|
// prefix of everything); other items use startsWith so sub-paths
|
|
91
91
|
// (e.g. /studio/collections/risks) still highlight their parent.
|
|
92
92
|
// Popover items with children are active when any of their children match.
|
|
93
|
-
|
|
93
|
+
// SvelteKit's pathname can come back with a trailing slash (`/studio/`)
|
|
94
|
+
// depending on config, so we normalize it before comparing.
|
|
95
|
+
const currentPath = $derived(page.url.pathname.replace(/\/$/, "") || "/");
|
|
94
96
|
function isItemActive(item: any): boolean {
|
|
95
97
|
if (item.navs) return item.navs.some((c: any) => isItemActive(c));
|
|
96
98
|
if (!item.href) return false;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
const itemHref = item.href.replace(/\/$/, "") || "/";
|
|
100
|
+
if (itemHref === "/studio") return currentPath === "/studio";
|
|
101
|
+
return currentPath === itemHref || currentPath.startsWith(itemHref + "/");
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
// onMount is enough — Studio gets remounted on login/logout (see
|
|
@@ -3,29 +3,43 @@
|
|
|
3
3
|
import { goto } from "$app/navigation";
|
|
4
4
|
import { ArrowRight } from "lucide-svelte";
|
|
5
5
|
import HomeFooter from "./homeFooter.svelte";
|
|
6
|
+
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
7
|
+
import { getExtensionUtils } from "../../extensions/extensionUtils";
|
|
8
|
+
import { getStudioContext } from "../../context";
|
|
9
|
+
|
|
10
|
+
const { lobb, ctx } = getStudioContext();
|
|
6
11
|
</script>
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
<!--
|
|
14
|
+
Any extension that registers a `pages.home` component takes over /studio
|
|
15
|
+
(e.g. an app-specific overview dashboard). ExtensionsComponents falls
|
|
16
|
+
back to rendering its children when nothing matches, so the default
|
|
17
|
+
Lobb welcome below stays as the safety net for projects without an
|
|
18
|
+
overriding extension.
|
|
19
|
+
-->
|
|
20
|
+
<ExtensionsComponents name="pages.home" utils={getExtensionUtils(lobb, ctx)}>
|
|
21
|
+
<div class="flex h-full flex-col">
|
|
22
|
+
<div
|
|
23
|
+
class="flex flex-1 w-full flex-col items-center justify-center gap-4 text-muted-foreground"
|
|
24
|
+
>
|
|
25
|
+
<div class="flex flex-col items-center justify-center p-4">
|
|
26
|
+
<div class="text-3xl">Welcome to Lobb!</div>
|
|
27
|
+
<div class="text-xs text-center">
|
|
28
|
+
Your journey starts here. Explore and make the most of your
|
|
29
|
+
experience.
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="flex flex-col items-center justify-center">
|
|
33
|
+
<Button
|
|
34
|
+
Icon={ArrowRight}
|
|
35
|
+
variant="outline"
|
|
36
|
+
class="h-7 px-3 text-xs font-normal"
|
|
37
|
+
onclick={() => goto("/studio/collections")}
|
|
38
|
+
>
|
|
39
|
+
Go to collections
|
|
40
|
+
</Button>
|
|
17
41
|
</div>
|
|
18
42
|
</div>
|
|
19
|
-
<
|
|
20
|
-
<Button
|
|
21
|
-
Icon={ArrowRight}
|
|
22
|
-
variant="outline"
|
|
23
|
-
class="h-7 px-3 text-xs font-normal"
|
|
24
|
-
onclick={() => goto("/studio/collections")}
|
|
25
|
-
>
|
|
26
|
-
Go to collections
|
|
27
|
-
</Button>
|
|
28
|
-
</div>
|
|
43
|
+
<HomeFooter />
|
|
29
44
|
</div>
|
|
30
|
-
|
|
31
|
-
</div>
|
|
45
|
+
</ExtensionsComponents>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import IMask from "imask";
|
|
3
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
4
|
+
import { cn } from "../../../utils.js";
|
|
5
|
+
|
|
6
|
+
// Locale-neutral grouping: space for thousands, dot for decimals
|
|
7
|
+
// (ISO 31-0). Norwegian, French, scientific contexts — same shape, and
|
|
8
|
+
// space can't be confused with comma-as-decimal vs comma-as-thousands.
|
|
9
|
+
const THOUSANDS = " ";
|
|
10
|
+
const RADIX = ".";
|
|
11
|
+
|
|
12
|
+
type Props = Omit<HTMLInputAttributes, "type" | "value" | "step"> & {
|
|
13
|
+
value?: number | string | null;
|
|
14
|
+
// 0 = integer-only, > 0 = allow that many decimal places.
|
|
15
|
+
scale?: number;
|
|
16
|
+
// When false, render a plain browser number input — no masking, no
|
|
17
|
+
// formatting. Default false so the component is safe to drop in
|
|
18
|
+
// anywhere; opt into grouping only where it makes sense.
|
|
19
|
+
groupDigits?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
value = $bindable<number | string | null | undefined>(),
|
|
24
|
+
scale = 0,
|
|
25
|
+
groupDigits = false,
|
|
26
|
+
class: className,
|
|
27
|
+
...rest
|
|
28
|
+
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
let inputEl: HTMLInputElement | undefined = $state();
|
|
31
|
+
let mask: ReturnType<typeof IMask> | null = null;
|
|
32
|
+
// Re-entrance guard: imask's `accept` event fires when we programmatically
|
|
33
|
+
// set unmaskedValue, which would create a sync→reflect loop with the
|
|
34
|
+
// "external value changed" effect below.
|
|
35
|
+
let applyingExternal = false;
|
|
36
|
+
|
|
37
|
+
$effect(() => {
|
|
38
|
+
if (!groupDigits) return;
|
|
39
|
+
if (!inputEl) return;
|
|
40
|
+
mask = IMask(inputEl, {
|
|
41
|
+
mask: Number,
|
|
42
|
+
thousandsSeparator: THOUSANDS,
|
|
43
|
+
radix: RADIX,
|
|
44
|
+
scale,
|
|
45
|
+
signed: true,
|
|
46
|
+
padFractionalZeros: false,
|
|
47
|
+
normalizeZeros: true,
|
|
48
|
+
// Allow the user to type either radix; imask remaps to the chosen
|
|
49
|
+
// one and ignores stray locale separators on input.
|
|
50
|
+
mapToRadix: [",", "."],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
mask.on("accept", () => {
|
|
54
|
+
if (applyingExternal) return;
|
|
55
|
+
const unmasked = mask!.unmaskedValue;
|
|
56
|
+
value = unmasked === "" ? null : Number(unmasked);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
applyingExternal = true;
|
|
60
|
+
mask.unmaskedValue = value == null ? "" : String(value);
|
|
61
|
+
applyingExternal = false;
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
mask?.destroy();
|
|
65
|
+
mask = null;
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Sync external value changes back into the mask (e.g. form reset, parent
|
|
70
|
+
// clearing the value). No-op when grouping is off.
|
|
71
|
+
$effect(() => {
|
|
72
|
+
const v = value;
|
|
73
|
+
if (!mask) return;
|
|
74
|
+
const next = v == null ? "" : String(v);
|
|
75
|
+
if (mask.unmaskedValue === next) return;
|
|
76
|
+
applyingExternal = true;
|
|
77
|
+
mask.unmaskedValue = next;
|
|
78
|
+
applyingExternal = false;
|
|
79
|
+
});
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
{#if groupDigits}
|
|
83
|
+
<input
|
|
84
|
+
bind:this={inputEl}
|
|
85
|
+
type="text"
|
|
86
|
+
inputmode={scale > 0 ? "decimal" : "numeric"}
|
|
87
|
+
class={cn(
|
|
88
|
+
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-base transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
89
|
+
className,
|
|
90
|
+
)}
|
|
91
|
+
{...rest}
|
|
92
|
+
/>
|
|
93
|
+
{:else}
|
|
94
|
+
<input
|
|
95
|
+
type="number"
|
|
96
|
+
step={scale > 0 ? "any" : "1"}
|
|
97
|
+
class={cn(
|
|
98
|
+
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-base transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
99
|
+
className,
|
|
100
|
+
)}
|
|
101
|
+
bind:value
|
|
102
|
+
{...rest}
|
|
103
|
+
/>
|
|
104
|
+
{/if}
|
|
@@ -23,7 +23,7 @@ import * as Icons from "lucide-svelte"
|
|
|
23
23
|
import { ContextMenu } from "bits-ui";
|
|
24
24
|
import * as Tooltip from "../components/ui/tooltip";
|
|
25
25
|
import * as Breadcrumb from "../components/ui/breadcrumb";
|
|
26
|
-
import { showDialog, type OpenDataTableDrawerProps } from "../actions";
|
|
26
|
+
import { showDialog, type OpenDataTableDrawerProps, type OpenDataTablePopupProps } from "../actions";
|
|
27
27
|
import { toast } from "svelte-sonner";
|
|
28
28
|
import type Drawer from "../components/drawer.svelte";
|
|
29
29
|
import { Switch } from "../components/ui/switch";
|
|
@@ -71,6 +71,7 @@ export interface ExtensionUtils {
|
|
|
71
71
|
toast: typeof toast;
|
|
72
72
|
showDialog: typeof showDialog;
|
|
73
73
|
openDataTableDrawer: (props: OpenDataTableDrawerProps) => void;
|
|
74
|
+
openDataTablePopup: (props: OpenDataTablePopupProps) => void;
|
|
74
75
|
emitEvent: (eventName: string, input: any) => Promise<any>;
|
|
75
76
|
components: Components;
|
|
76
77
|
mediaQueries: typeof mediaQueries;
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
import type { LobbClient } from "@lobb-js/sdk";
|
|
8
8
|
import type { CTX } from "../store.types";
|
|
9
9
|
import { toast } from "svelte-sonner";
|
|
10
|
-
import { showDialog, openDataTableDrawer } from "../actions";
|
|
10
|
+
import { showDialog, openDataTableDrawer, openDataTablePopup } from "../actions";
|
|
11
11
|
import { emitEvent } from "../eventSystem";
|
|
12
12
|
import { Button } from "../components/ui/button";
|
|
13
13
|
import { Input } from "../components/ui/input";
|
|
@@ -68,6 +68,7 @@ export function getExtensionUtils(lobb: LobbClient, ctx: CTX): ExtensionUtils {
|
|
|
68
68
|
toast: toast,
|
|
69
69
|
showDialog: showDialog,
|
|
70
70
|
openDataTableDrawer: (props) => openDataTableDrawer({ lobb, ctx }, props),
|
|
71
|
+
openDataTablePopup: (props) => openDataTablePopup({ lobb, ctx }, props),
|
|
71
72
|
emitEvent: (eventName, input) => emitEvent({ lobb, ctx }, eventName, input),
|
|
72
73
|
components: getComponents(),
|
|
73
74
|
mediaQueries: mediaQueries,
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import * as Breadcrumb from "./ui/breadcrumb";
|
|
3
|
-
import { mediaQueries } from "../utils";
|
|
4
|
-
import { page } from "$app/state";
|
|
5
|
-
import { goto } from "$app/navigation";
|
|
6
|
-
|
|
7
|
-
const isSmall = $derived(!mediaQueries.sm.current);
|
|
8
|
-
const pathNames = $derived(
|
|
9
|
-
page.url.pathname
|
|
10
|
-
.replace("/studio", "")
|
|
11
|
-
.split("/")
|
|
12
|
-
.filter((el: any) => el !== "")
|
|
13
|
-
.slice(0, 3),
|
|
14
|
-
);
|
|
15
|
-
</script>
|
|
16
|
-
|
|
17
|
-
{#if isSmall}
|
|
18
|
-
<div class="text-muted-foreground text-sm">
|
|
19
|
-
{pathNames[pathNames.length - 1] || "Home"}
|
|
20
|
-
</div>
|
|
21
|
-
{:else}
|
|
22
|
-
<Breadcrumb.Root>
|
|
23
|
-
<Breadcrumb.List class="flex-nowrap">
|
|
24
|
-
<Breadcrumb.Item>
|
|
25
|
-
{#if pathNames.length === 0}
|
|
26
|
-
<Breadcrumb.Page>Home</Breadcrumb.Page>
|
|
27
|
-
{:else}
|
|
28
|
-
<Breadcrumb.Link
|
|
29
|
-
class="cursor-pointer"
|
|
30
|
-
onclick={() => goto("/studio")}
|
|
31
|
-
>
|
|
32
|
-
Home
|
|
33
|
-
</Breadcrumb.Link>
|
|
34
|
-
<Breadcrumb.Separator />
|
|
35
|
-
{/if}
|
|
36
|
-
</Breadcrumb.Item>
|
|
37
|
-
{#each pathNames as path, index}
|
|
38
|
-
{@const isLastElement = pathNames.length - 1 === index}
|
|
39
|
-
{@const currentFullPaths = pathNames
|
|
40
|
-
.slice(0, index + 1)
|
|
41
|
-
.join("/")}
|
|
42
|
-
<Breadcrumb.Item>
|
|
43
|
-
{#if isLastElement}
|
|
44
|
-
<Breadcrumb.Page>{path}</Breadcrumb.Page>
|
|
45
|
-
{:else}
|
|
46
|
-
<Breadcrumb.Link
|
|
47
|
-
class="cursor-pointer"
|
|
48
|
-
onclick={() =>
|
|
49
|
-
goto(`/studio/${currentFullPaths}`)}
|
|
50
|
-
>
|
|
51
|
-
{path}
|
|
52
|
-
</Breadcrumb.Link>
|
|
53
|
-
{/if}
|
|
54
|
-
</Breadcrumb.Item>
|
|
55
|
-
{#if !isLastElement}
|
|
56
|
-
<Breadcrumb.Separator />
|
|
57
|
-
{/if}
|
|
58
|
-
{/each}
|
|
59
|
-
</Breadcrumb.List>
|
|
60
|
-
</Breadcrumb.Root>
|
|
61
|
-
{/if}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import * as Tooltip from "./ui/tooltip";
|
|
3
|
-
import Button from "./ui/button/button.svelte";
|
|
4
|
-
import { Menu, Moon, Sun } from "lucide-svelte";
|
|
5
|
-
import BreadCrumbs from "./breadCrumbs.svelte";
|
|
6
|
-
import { toggleMode, mode } from "mode-watcher";
|
|
7
|
-
import { mediaQueries } from "../utils";
|
|
8
|
-
import { expandMiniSideBar } from "./miniSidebar.svelte";
|
|
9
|
-
|
|
10
|
-
let isSmallScreen = $derived(!mediaQueries.sm.current);
|
|
11
|
-
</script>
|
|
12
|
-
|
|
13
|
-
<div
|
|
14
|
-
class="flex items-center justify-between border-b border-input bg-background px-3"
|
|
15
|
-
>
|
|
16
|
-
<div class="flex items-center gap-4">
|
|
17
|
-
{#if isSmallScreen}
|
|
18
|
-
<Menu
|
|
19
|
-
class="h-10 text-muted-foreground hover:bg-transparent cursor-pointer hover:text-foreground"
|
|
20
|
-
style="left: 0.6rem; top: 0rem;"
|
|
21
|
-
size="18"
|
|
22
|
-
onclick={() => expandMiniSideBar()}
|
|
23
|
-
/>
|
|
24
|
-
{/if}
|
|
25
|
-
<BreadCrumbs />
|
|
26
|
-
</div>
|
|
27
|
-
<div class="flex h-full items-center gap-3">
|
|
28
|
-
<Tooltip.Root>
|
|
29
|
-
<Tooltip.Trigger>
|
|
30
|
-
<Button
|
|
31
|
-
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
32
|
-
variant="ghost"
|
|
33
|
-
size="icon"
|
|
34
|
-
onclick={toggleMode}
|
|
35
|
-
Icon={$mode === "light" ? Moon : Sun}
|
|
36
|
-
></Button>
|
|
37
|
-
</Tooltip.Trigger>
|
|
38
|
-
<Tooltip.Content side="bottom" sideOffset={7.5}
|
|
39
|
-
>{$mode === "light"
|
|
40
|
-
? "Night Mode"
|
|
41
|
-
: "Light Mode"}</Tooltip.Content
|
|
42
|
-
>
|
|
43
|
-
</Tooltip.Root>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|