@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.
Files changed (41) hide show
  1. package/dist/actions.d.ts +11 -0
  2. package/dist/actions.js +16 -0
  3. package/dist/components/Studio.svelte +1 -10
  4. package/dist/components/dataTable/dataTable.svelte +20 -2
  5. package/dist/components/dataTable/fieldCell.svelte +3 -0
  6. package/dist/components/dataTable/filter.svelte +0 -15
  7. package/dist/components/dataTable/numberCell.svelte +28 -0
  8. package/dist/components/dataTable/numberCell.svelte.d.ts +7 -0
  9. package/dist/components/dataTablePopup/dataTablePopup.svelte +84 -0
  10. package/dist/components/dataTablePopup/dataTablePopup.svelte.d.ts +17 -0
  11. package/dist/components/detailView/detailView.svelte +6 -1
  12. package/dist/components/detailView/fieldInput.svelte +7 -5
  13. package/dist/components/miniSidebar.svelte +6 -3
  14. package/dist/components/routes/extensions/extension.svelte +1 -1
  15. package/dist/components/routes/home.svelte +35 -21
  16. package/dist/components/ui/input/numberInput.svelte +104 -0
  17. package/dist/components/ui/input/numberInput.svelte.d.ts +9 -0
  18. package/dist/extensions/extension.types.d.ts +2 -1
  19. package/dist/extensions/extensionUtils.js +2 -1
  20. package/package.json +3 -2
  21. package/src/lib/actions.ts +28 -0
  22. package/src/lib/components/Studio.svelte +1 -10
  23. package/src/lib/components/dataTable/dataTable.svelte +20 -2
  24. package/src/lib/components/dataTable/fieldCell.svelte +3 -0
  25. package/src/lib/components/dataTable/filter.svelte +0 -15
  26. package/src/lib/components/dataTable/numberCell.svelte +28 -0
  27. package/src/lib/components/dataTablePopup/dataTablePopup.svelte +84 -0
  28. package/src/lib/components/detailView/detailView.svelte +6 -1
  29. package/src/lib/components/detailView/fieldInput.svelte +7 -5
  30. package/src/lib/components/miniSidebar.svelte +6 -3
  31. package/src/lib/components/routes/extensions/extension.svelte +1 -1
  32. package/src/lib/components/routes/home.svelte +35 -21
  33. package/src/lib/components/ui/input/numberInput.svelte +104 -0
  34. package/src/lib/extensions/extension.types.ts +2 -1
  35. package/src/lib/extensions/extensionUtils.ts +2 -1
  36. package/dist/components/breadCrumbs.svelte +0 -61
  37. package/dist/components/breadCrumbs.svelte.d.ts +0 -3
  38. package/dist/components/header.svelte +0 -45
  39. package/dist/components/header.svelte.d.ts +0 -6
  40. package/src/lib/components/breadCrumbs.svelte +0 -61
  41. package/src/lib/components/header.svelte +0 -45
package/dist/actions.d.ts CHANGED
@@ -11,7 +11,18 @@ export interface OpenDataTableDrawerProps {
11
11
  position?: "side" | "bottom";
12
12
  tabs?: CollectionTab[];
13
13
  }
14
+ export interface OpenDataTablePopupProps {
15
+ collectionName: string;
16
+ filter?: Record<string, any>;
17
+ sort?: Record<string, "asc" | "desc">;
18
+ limit?: number;
19
+ title?: string;
20
+ showHeader?: boolean;
21
+ showFooter?: boolean;
22
+ tabs?: CollectionTab[];
23
+ }
14
24
  export declare function showDialog(title: string, description: string): Promise<boolean>;
15
25
  export declare function openCreateDetailView(studioContext: StudioContext, props: CreateDetailViewProp): void;
16
26
  export declare function openUpdateDetailView(studioContext: StudioContext, props: UpdateDetailViewProp): Promise<void>;
17
27
  export declare function openDataTableDrawer(studioContext: StudioContext, props: OpenDataTableDrawerProps): void;
28
+ export declare function openDataTablePopup(studioContext: StudioContext, props: OpenDataTablePopupProps): void;
package/dist/actions.js CHANGED
@@ -3,6 +3,7 @@ import ConfirmationDialog from "./components/confirmationDialog/confirmationDial
3
3
  import CreateDetailView from "./components/detailView/create/createDetailView.svelte";
4
4
  import UpdateDetailView from "./components/detailView/update/updateDetailView.svelte";
5
5
  import DataTableDrawer from "./components/dataTableDrawer/dataTableDrawer.svelte";
6
+ import DataTablePopup from "./components/dataTablePopup/dataTablePopup.svelte";
6
7
  import { createStudioContextMap } from "./context";
7
8
  import { getCollectionParamsFields } from "./components/dataTable/utils";
8
9
  export function showDialog(title, description) {
@@ -80,3 +81,18 @@ export function openDataTableDrawer(studioContext, props) {
80
81
  },
81
82
  });
82
83
  }
84
+ export function openDataTablePopup(studioContext, props) {
85
+ const targetElement = document.querySelector("main");
86
+ if (!targetElement)
87
+ throw new Error("main html element doesn't exist");
88
+ const mounted = mount(DataTablePopup, {
89
+ target: targetElement,
90
+ context: createStudioContextMap(studioContext),
91
+ props: {
92
+ ...props,
93
+ onClose: async () => {
94
+ await unmount(mounted, { outro: true });
95
+ },
96
+ },
97
+ });
98
+ }
@@ -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="second_grid">
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,7 @@
1
+ interface Props {
2
+ value: any;
3
+ groupDigits?: boolean;
4
+ }
5
+ declare const NumberCell: import("svelte").Component<Props, {}, "">;
6
+ type NumberCell = ReturnType<typeof NumberCell>;
7
+ export default NumberCell;
@@ -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>
@@ -0,0 +1,17 @@
1
+ import type { TableProps } from "../dataTable/table.svelte";
2
+ import type { CollectionTab } from "../../store.types";
3
+ interface Props {
4
+ collectionName: string;
5
+ filter?: Record<string, any>;
6
+ sort?: Record<string, "asc" | "desc">;
7
+ limit?: number;
8
+ title?: string;
9
+ showHeader?: boolean;
10
+ showFooter?: boolean;
11
+ tableProps?: Partial<TableProps>;
12
+ tabs?: CollectionTab[];
13
+ onClose?: () => void;
14
+ }
15
+ declare const DataTablePopup: import("svelte").Component<Props, {}, "">;
16
+ type DataTablePopup = ReturnType<typeof DataTablePopup>;
17
+ export default DataTablePopup;
@@ -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
- <Input
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
- type="number"
264
- step="any"
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="number"
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
- const currentPath = $derived(page.url.pathname);
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
- if (item.href === "/studio") return currentPath === "/studio";
98
- return currentPath === item.href || currentPath.startsWith(item.href + "/");
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
@@ -8,7 +8,7 @@
8
8
  const { lobb, ctx } = getStudioContext();
9
9
  </script>
10
10
 
11
- <div class="grid overflow-auto bg-background">
11
+ <div class="grid h-full overflow-auto bg-background">
12
12
  {#key extension && page}
13
13
  <ExtensionsComponents
14
14
  name="pages.{page}"
@@ -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
- <div class="flex flex-col">
9
- <div
10
- class="flex flex-1 w-full flex-col items-center justify-center gap-4 text-muted-foreground"
11
- >
12
- <div class="flex flex-col items-center justify-center p-4">
13
- <div class="text-3xl">Welcome to Lobb!</div>
14
- <div class="text-xs text-center">
15
- Your journey starts here. Explore and make the most of your
16
- experience.
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
- <div class="flex flex-col items-center justify-center">
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
- <HomeFooter />
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}
@@ -0,0 +1,9 @@
1
+ import type { HTMLInputAttributes } from "svelte/elements";
2
+ type Props = Omit<HTMLInputAttributes, "type" | "value" | "step"> & {
3
+ value?: number | string | null;
4
+ scale?: number;
5
+ groupDigits?: boolean;
6
+ };
7
+ declare const NumberInput: import("svelte").Component<Props, {}, "value">;
8
+ type NumberInput = ReturnType<typeof NumberInput>;
9
+ export default NumberInput;
@@ -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 { Switch } from "../components/ui/switch";
29
29
  export interface Components {
@@ -67,6 +67,7 @@ export interface ExtensionUtils {
67
67
  toast: typeof toast;
68
68
  showDialog: typeof showDialog;
69
69
  openDataTableDrawer: (props: OpenDataTableDrawerProps) => void;
70
+ openDataTablePopup: (props: OpenDataTablePopupProps) => void;
70
71
  emitEvent: (eventName: string, input: any) => Promise<any>;
71
72
  components: Components;
72
73
  mediaQueries: typeof mediaQueries;
@@ -1,5 +1,5 @@
1
1
  import { toast } from "svelte-sonner";
2
- import { showDialog, openDataTableDrawer } from "../actions";
2
+ import { showDialog, openDataTableDrawer, openDataTablePopup } from "../actions";
3
3
  import { emitEvent } from "../eventSystem";
4
4
  import { Button } from "../components/ui/button";
5
5
  import { Input } from "../components/ui/input";
@@ -58,6 +58,7 @@ export function getExtensionUtils(lobb, ctx) {
58
58
  toast: toast,
59
59
  showDialog: showDialog,
60
60
  openDataTableDrawer: (props) => openDataTableDrawer({ lobb, ctx }, props),
61
+ openDataTablePopup: (props) => openDataTablePopup({ lobb, ctx }, props),
61
62
  emitEvent: (eventName, input) => emitEvent({ lobb, ctx }, eventName, input),
62
63
  components: getComponents(),
63
64
  mediaQueries: mediaQueries,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/studio",
3
3
  "license": "UNLICENSED",
4
- "version": "0.29.1",
4
+ "version": "0.31.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -42,7 +42,7 @@
42
42
  "postpublish": "./scripts/postpublish.sh"
43
43
  },
44
44
  "devDependencies": {
45
- "@lobb-js/core": "^0.32.1",
45
+ "@lobb-js/core": "^0.33.0",
46
46
  "@chromatic-com/storybook": "^4.1.2",
47
47
  "@storybook/addon-a11y": "^10.0.1",
48
48
  "@storybook/addon-docs": "^10.0.1",
@@ -101,6 +101,7 @@
101
101
  "codemirror": "^6.0.2",
102
102
  "fflate": "^0.8.2",
103
103
  "fuse.js": "^7.3.0",
104
+ "imask": "^7.6.1",
104
105
  "javascript-time-ago": "^2.6.4",
105
106
  "json-stable-stringify": "^1.3.0",
106
107
  "lucide-svelte": "^0.488.0",
@@ -3,6 +3,7 @@ import ConfirmationDialog from "./components/confirmationDialog/confirmationDial
3
3
  import CreateDetailView from "./components/detailView/create/createDetailView.svelte";
4
4
  import UpdateDetailView from "./components/detailView/update/updateDetailView.svelte";
5
5
  import DataTableDrawer from "./components/dataTableDrawer/dataTableDrawer.svelte";
6
+ import DataTablePopup from "./components/dataTablePopup/dataTablePopup.svelte";
6
7
  import type { CreateDetailViewProp } from "./components/detailView/create/createDetailView.svelte";
7
8
  import type { UpdateDetailViewProp } from "./components/detailView/update/updateDetailView.svelte";
8
9
  import type { StudioContext } from "./context";
@@ -20,6 +21,17 @@ export interface OpenDataTableDrawerProps {
20
21
  tabs?: CollectionTab[];
21
22
  }
22
23
 
24
+ export interface OpenDataTablePopupProps {
25
+ collectionName: string;
26
+ filter?: Record<string, any>;
27
+ sort?: Record<string, "asc" | "desc">;
28
+ limit?: number;
29
+ title?: string;
30
+ showHeader?: boolean;
31
+ showFooter?: boolean;
32
+ tabs?: CollectionTab[];
33
+ }
34
+
23
35
  export function showDialog(title: string, description: string): Promise<boolean> {
24
36
  return new Promise((resolve) => {
25
37
  const targetElement = document.querySelector("main");
@@ -99,3 +111,19 @@ export function openDataTableDrawer(studioContext: StudioContext, props: OpenDat
99
111
  },
100
112
  });
101
113
  }
114
+
115
+ export function openDataTablePopup(studioContext: StudioContext, props: OpenDataTablePopupProps) {
116
+ const targetElement = document.querySelector("main");
117
+ if (!targetElement) throw new Error("main html element doesn't exist");
118
+
119
+ const mounted = mount(DataTablePopup, {
120
+ target: targetElement,
121
+ context: createStudioContextMap(studioContext),
122
+ props: {
123
+ ...props,
124
+ onClose: async () => {
125
+ await unmount(mounted, { outro: true });
126
+ },
127
+ },
128
+ });
129
+ }