@instantdb/components 0.0.1

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 (128) hide show
  1. package/.env +2 -0
  2. package/.turbo/turbo-build.log +18 -0
  3. package/README.md +78 -0
  4. package/app/App.css +38 -0
  5. package/app/App.tsx +61 -0
  6. package/app/index.css +18 -0
  7. package/app/main.tsx +10 -0
  8. package/dist/components/StyleMe.d.ts +15 -0
  9. package/dist/components/StyleMe.d.ts.map +1 -0
  10. package/dist/components/error-boundary.d.ts +17 -0
  11. package/dist/components/error-boundary.d.ts.map +1 -0
  12. package/dist/components/explorer/edit-namespace-dialog.d.ts +14 -0
  13. package/dist/components/explorer/edit-namespace-dialog.d.ts.map +1 -0
  14. package/dist/components/explorer/edit-row-dialog.d.ts +10 -0
  15. package/dist/components/explorer/edit-row-dialog.d.ts.map +1 -0
  16. package/dist/components/explorer/expandable-deleted-attr.d.ts +15 -0
  17. package/dist/components/explorer/expandable-deleted-attr.d.ts.map +1 -0
  18. package/dist/components/explorer/explorer-layout.d.ts +8 -0
  19. package/dist/components/explorer/explorer-layout.d.ts.map +1 -0
  20. package/dist/components/explorer/index.d.ts +44 -0
  21. package/dist/components/explorer/index.d.ts.map +1 -0
  22. package/dist/components/explorer/inner-explorer.d.ts +16 -0
  23. package/dist/components/explorer/inner-explorer.d.ts.map +1 -0
  24. package/dist/components/explorer/new-namespace-dialog.d.ts +10 -0
  25. package/dist/components/explorer/new-namespace-dialog.d.ts.map +1 -0
  26. package/dist/components/explorer/query-inspector.d.ts +11 -0
  27. package/dist/components/explorer/query-inspector.d.ts.map +1 -0
  28. package/dist/components/explorer/recently-deleted.d.ts +36 -0
  29. package/dist/components/explorer/recently-deleted.d.ts.map +1 -0
  30. package/dist/components/explorer/search-input.d.ts +9 -0
  31. package/dist/components/explorer/search-input.d.ts.map +1 -0
  32. package/dist/components/explorer/table-components.d.ts +16 -0
  33. package/dist/components/explorer/table-components.d.ts.map +1 -0
  34. package/dist/components/explorer/view-settings.d.ts +10 -0
  35. package/dist/components/explorer/view-settings.d.ts.map +1 -0
  36. package/dist/components/rosePineDawnTheme.d.ts +13 -0
  37. package/dist/components/rosePineDawnTheme.d.ts.map +1 -0
  38. package/dist/components/select.d.ts +16 -0
  39. package/dist/components/select.d.ts.map +1 -0
  40. package/dist/components/toast.d.ts +4 -0
  41. package/dist/components/toast.d.ts.map +1 -0
  42. package/dist/components/ui.d.ts +336 -0
  43. package/dist/components/ui.d.ts.map +1 -0
  44. package/dist/config.d.ts +14 -0
  45. package/dist/config.d.ts.map +1 -0
  46. package/dist/hooks/explorer.d.ts +29 -0
  47. package/dist/hooks/explorer.d.ts.map +1 -0
  48. package/dist/hooks/useAttrNotes.d.ts +10 -0
  49. package/dist/hooks/useAttrNotes.d.ts.map +1 -0
  50. package/dist/hooks/useClickOutside.d.ts +3 -0
  51. package/dist/hooks/useClickOutside.d.ts.map +1 -0
  52. package/dist/hooks/useColumnVisibility.d.ts +12 -0
  53. package/dist/hooks/useColumnVisibility.d.ts.map +1 -0
  54. package/dist/hooks/useEditBlobConstraints.d.ts +32 -0
  55. package/dist/hooks/useEditBlobConstraints.d.ts.map +1 -0
  56. package/dist/hooks/useExplorerHistory.d.ts +1 -0
  57. package/dist/hooks/useExplorerHistory.d.ts.map +1 -0
  58. package/dist/hooks/useIsOverflow.d.ts +6 -0
  59. package/dist/hooks/useIsOverflow.d.ts.map +1 -0
  60. package/dist/hooks/useLocalStorage.d.ts +2 -0
  61. package/dist/hooks/useLocalStorage.d.ts.map +1 -0
  62. package/dist/hooks/useMonacoJSONSchema.d.ts +3 -0
  63. package/dist/hooks/useMonacoJSONSchema.d.ts.map +1 -0
  64. package/dist/hooks/useStableDB.d.ts +7 -0
  65. package/dist/hooks/useStableDB.d.ts.map +1 -0
  66. package/dist/index.cjs +15 -0
  67. package/dist/index.d.ts +7 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +9270 -0
  70. package/dist/schema.d.ts +5 -0
  71. package/dist/schema.d.ts.map +1 -0
  72. package/dist/style.css +1 -0
  73. package/dist/types.d.ts +241 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/dist/utils/format.d.ts +2 -0
  76. package/dist/utils/format.d.ts.map +1 -0
  77. package/dist/utils/indexingJobs.d.ts +24 -0
  78. package/dist/utils/indexingJobs.d.ts.map +1 -0
  79. package/dist/utils/parsePermsJSON.d.ts +11 -0
  80. package/dist/utils/parsePermsJSON.d.ts.map +1 -0
  81. package/dist/utils/renames.d.ts +3 -0
  82. package/dist/utils/renames.d.ts.map +1 -0
  83. package/dist/utils/tableWidthSize.d.ts +9 -0
  84. package/dist/utils/tableWidthSize.d.ts.map +1 -0
  85. package/index.html +13 -0
  86. package/package.json +109 -0
  87. package/src/components/StyleMe.tsx +97 -0
  88. package/src/components/error-boundary.tsx +76 -0
  89. package/src/components/explorer/edit-namespace-dialog.tsx +1886 -0
  90. package/src/components/explorer/edit-row-dialog.tsx +1151 -0
  91. package/src/components/explorer/expandable-deleted-attr.tsx +170 -0
  92. package/src/components/explorer/explorer-layout.tsx +156 -0
  93. package/src/components/explorer/index.tsx +217 -0
  94. package/src/components/explorer/inner-explorer.tsx +1341 -0
  95. package/src/components/explorer/new-namespace-dialog.tsx +54 -0
  96. package/src/components/explorer/query-inspector.tsx +394 -0
  97. package/src/components/explorer/recently-deleted.tsx +344 -0
  98. package/src/components/explorer/search-input.tsx +358 -0
  99. package/src/components/explorer/table-components.tsx +341 -0
  100. package/src/components/explorer/view-settings.tsx +75 -0
  101. package/src/components/rosePineDawnTheme.ts +45 -0
  102. package/src/components/select.tsx +198 -0
  103. package/src/components/toast.tsx +18 -0
  104. package/src/components/ui.tsx +1561 -0
  105. package/src/config.ts +61 -0
  106. package/src/hooks/explorer.tsx +125 -0
  107. package/src/hooks/useAttrNotes.ts +27 -0
  108. package/src/hooks/useClickOutside.ts +23 -0
  109. package/src/hooks/useColumnVisibility.ts +39 -0
  110. package/src/hooks/useEditBlobConstraints.ts +185 -0
  111. package/src/hooks/useExplorerHistory.ts +0 -0
  112. package/src/hooks/useIsOverflow.ts +24 -0
  113. package/src/hooks/useLocalStorage.ts +51 -0
  114. package/src/hooks/useMonacoJSONSchema.ts +41 -0
  115. package/src/hooks/useStableDB.ts +30 -0
  116. package/src/index.tsx +8 -0
  117. package/src/schema.ts +285 -0
  118. package/src/style.css +5 -0
  119. package/src/types.ts +359 -0
  120. package/src/utils/format.ts +13 -0
  121. package/src/utils/indexingJobs.ts +126 -0
  122. package/src/utils/parsePermsJSON.ts +35 -0
  123. package/src/utils/renames.ts +42 -0
  124. package/src/utils/tableWidthSize.ts +62 -0
  125. package/tailwind.config.cjs +42 -0
  126. package/tsconfig.json +22 -0
  127. package/vite-env.d.ts +1 -0
  128. package/vite.config.ts +49 -0
package/src/config.ts ADDED
@@ -0,0 +1,61 @@
1
+ export const isBrowser = typeof window != 'undefined';
2
+
3
+ const devBackend = getLocal('devBackend');
4
+
5
+ let localPort = '8888';
6
+
7
+ if (devBackend && isBrowser) {
8
+ const portOverride = new URL(location.href).searchParams.get('port');
9
+ if (portOverride) {
10
+ localPort = portOverride;
11
+ }
12
+ }
13
+
14
+ export const config = {
15
+ apiURI: getLocal('devBackend')
16
+ ? `http://localhost:${localPort}`
17
+ : `https://api.instantdb.com`,
18
+ websocketURI: getLocal('devBackend')
19
+ ? `ws://localhost:${localPort}/runtime/session`
20
+ : `wss://api.instantdb.com/runtime/session`,
21
+ };
22
+
23
+ export const isTouchDevice =
24
+ typeof window !== 'undefined' && 'ontouchstart' in window;
25
+
26
+ export function getLocal(k: string) {
27
+ if (!isBrowser) {
28
+ return null;
29
+ }
30
+
31
+ try {
32
+ const raw = localStorage.getItem(k);
33
+
34
+ return raw ? JSON.parse(raw) : null;
35
+ } catch (e) {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ export function setLocal(k: string, v: any) {
41
+ if (!isBrowser) {
42
+ return;
43
+ }
44
+
45
+ try {
46
+ localStorage.setItem(k, JSON.stringify(v));
47
+ } catch (e) {
48
+ return;
49
+ }
50
+ }
51
+
52
+ export const localStorageFlagPrefix = `__instant__flag__`;
53
+
54
+ export const cliOauthParamName = '_cli_oauth_ticket';
55
+
56
+ export const discordInviteUrl = 'https://discord.com/invite/VU53p7uQcE';
57
+
58
+ export const discordOAuthAppsFeedbackInviteUrl =
59
+ 'https://discord.gg/GrvbPTBDEX';
60
+
61
+ export const bugsAndQuestionsInviteUrl = 'https://discord.gg/unA5vyV6mP';
@@ -0,0 +1,125 @@
1
+ import { InstantReactWebDatabase } from '@instantdb/react';
2
+ import { useEffect, useState } from 'react';
3
+ import { DBAttr, SchemaNamespace } from '@lib/types';
4
+ import { dbAttrsToExplorerSchema } from '@lib/schema';
5
+
6
+ export type SearchFilterOp =
7
+ | '='
8
+ | '$ilike'
9
+ | '$like'
10
+ | '$gt'
11
+ | '$lt'
12
+ | '$isNull';
13
+ export type SearchFilter = [string, SearchFilterOp, any];
14
+
15
+ function makeWhere(
16
+ navWhere: null | undefined | [string, any],
17
+ searchFilters: null | undefined | SearchFilter[],
18
+ ) {
19
+ const where: { [key: string]: any } = {};
20
+ if (navWhere) {
21
+ where[navWhere[0]] = navWhere[1];
22
+ }
23
+ if (searchFilters?.length) {
24
+ where.or = searchFilters.map(([attr, op, val]) => {
25
+ switch (op) {
26
+ case '=':
27
+ return { [attr]: val };
28
+ case '$isNull':
29
+ return { [attr]: { [op]: true } };
30
+ default:
31
+ return { [attr]: { [op]: val } };
32
+ }
33
+ });
34
+ }
35
+ return where;
36
+ }
37
+
38
+ // HOOKS
39
+ export function useNamespacesQuery(
40
+ db: InstantReactWebDatabase<any>,
41
+ selectedNs?: SchemaNamespace,
42
+ navWhere?: [string, any],
43
+ searchFilters?: SearchFilter[],
44
+ limit?: number,
45
+ offset?: number,
46
+ sortAttr?: string,
47
+ sortAsc?: boolean,
48
+ ) {
49
+ const direction: 'asc' | 'desc' = sortAsc ? 'asc' : 'desc';
50
+
51
+ const where = makeWhere(navWhere, searchFilters);
52
+
53
+ const iql = selectedNs
54
+ ? {
55
+ [selectedNs.name]: {
56
+ ...Object.fromEntries(
57
+ selectedNs.attrs
58
+ .filter((a) => a.type === 'ref')
59
+ .map((a) => [a.name, { $: { fields: ['id'] } }]),
60
+ ),
61
+ $: {
62
+ ...(where ? { where: where } : {}),
63
+ ...(limit ? { limit } : {}),
64
+ ...(offset ? { offset } : {}),
65
+ ...(sortAttr ? { order: { [sortAttr]: direction } } : {}),
66
+ },
67
+ },
68
+ }
69
+ : {};
70
+
71
+ const itemsRes = db.useQuery(iql);
72
+
73
+ const allRes = db.useQuery(
74
+ selectedNs
75
+ ? {
76
+ [selectedNs.name]: {
77
+ $: {
78
+ aggregate: 'count',
79
+ ...(where ? { where: where } : {}),
80
+ },
81
+ },
82
+ }
83
+ : {},
84
+ );
85
+
86
+ // @ts-expect-error: admin-only feature
87
+ const allCount = allRes.aggregate?.[selectedNs?.name ?? '']?.count ?? null;
88
+
89
+ return {
90
+ itemsRes,
91
+ allCount,
92
+ };
93
+ }
94
+ export function useSchemaQuery(db: InstantReactWebDatabase<any>) {
95
+ const [state, setState] = useState<
96
+ | {
97
+ namespaces: SchemaNamespace[];
98
+ attrs: Record<string, DBAttr>;
99
+ }
100
+ | { namespaces: null; attrs: null }
101
+ >({ namespaces: null, attrs: null });
102
+
103
+ // (XXX)
104
+ // This is a hack so we can listen to all attr changes
105
+ //
106
+ // Context:
107
+ // The backend only sends attr changes to relevant queries.
108
+ // The ___explorer__ is a dummy query, which refreshes when _anything_
109
+ // happens.
110
+ //
111
+ // In the future, we may want a special `attr-changed` event.
112
+ db.useQuery({ ____explorer___: {} });
113
+
114
+ useEffect(() => {
115
+ function onAttrs(_oAttrs: Record<string, DBAttr>) {
116
+ setState({
117
+ attrs: _oAttrs,
118
+ namespaces: dbAttrsToExplorerSchema(_oAttrs),
119
+ });
120
+ }
121
+ return db._core._reactor.subscribeAttrs(onAttrs);
122
+ }, [db]);
123
+
124
+ return state;
125
+ }
@@ -0,0 +1,27 @@
1
+ import { useState } from 'react';
2
+
3
+ type Note = {
4
+ message: string;
5
+ };
6
+
7
+ export const useAttrNotes = () => {
8
+ const [notes, setNotes] = useState<Record<string, Note>>({});
9
+
10
+ const setNote = (id: string, message: string) => {
11
+ setNotes((notes) => ({ ...notes, [id]: { message } }));
12
+ };
13
+
14
+ const removeNote = (id: string) => {
15
+ setNotes((notes) => {
16
+ const newNotes = { ...notes };
17
+ delete newNotes[id];
18
+ return newNotes;
19
+ });
20
+ };
21
+
22
+ return {
23
+ notes,
24
+ setNote,
25
+ removeNote,
26
+ };
27
+ };
@@ -0,0 +1,23 @@
1
+ import { RefObject, useEffect } from 'react';
2
+
3
+ export function useClickOutside(
4
+ ref: RefObject<HTMLElement>,
5
+ callback: () => void,
6
+ ) {
7
+ const handleClick = (e: MouseEvent) => {
8
+ if (
9
+ ref.current &&
10
+ e.target instanceof HTMLElement &&
11
+ !ref.current.contains(e.target)
12
+ ) {
13
+ callback();
14
+ }
15
+ };
16
+
17
+ useEffect(() => {
18
+ document.addEventListener('click', handleClick);
19
+ return () => {
20
+ document.removeEventListener('click', handleClick);
21
+ };
22
+ });
23
+ }
@@ -0,0 +1,39 @@
1
+ import { VisibilityState } from '@tanstack/react-table';
2
+ import { useEffect, useState } from 'react';
3
+ import { SchemaAttr } from '../types';
4
+
5
+ const getColumnVisibilty = (appId: string) => {
6
+ const possible = localStorage.getItem(`columnVisibility_${appId}`);
7
+ if (!possible) {
8
+ return {};
9
+ }
10
+ try {
11
+ return JSON.parse(possible);
12
+ } catch (error) {
13
+ console.error('Failed to parse column visibility', error);
14
+ return {};
15
+ }
16
+ };
17
+
18
+ export const useColumnVisibility = (props: {
19
+ appId: string;
20
+ namespaceId?: string;
21
+ attrs: SchemaAttr[] | undefined;
22
+ }) => {
23
+ const [visibility, setVisibility] = useState<VisibilityState>(
24
+ getColumnVisibilty(props.appId),
25
+ );
26
+
27
+ useEffect(() => {
28
+ localStorage.setItem(
29
+ `columnVisibility_${props.appId}`,
30
+ JSON.stringify(visibility),
31
+ );
32
+ }, [props.appId, visibility]);
33
+
34
+ useEffect(() => {
35
+ setVisibility(getColumnVisibilty(props.appId));
36
+ }, [props.appId]);
37
+
38
+ return { visibility, setVisibility, attrs: props.attrs } as const;
39
+ };
@@ -0,0 +1,185 @@
1
+ import { useEffect, useState, useRef, useMemo } from 'react';
2
+ import { CheckedDataType, InstantIndexingJob, SchemaAttr } from '../types';
3
+ import { jobFetchLoop } from '../utils/indexingJobs';
4
+ import { useExplorerProps } from '@lib/components/explorer';
5
+
6
+ type JobConstraintTypes = 'require' | 'index' | 'unique' | 'type';
7
+
8
+ export type PendingJob = {
9
+ jobType: InstantIndexingJob['job_type'];
10
+ checkedDataType?: CheckedDataType | null | undefined;
11
+ };
12
+
13
+ export const useEditBlobConstraints = ({
14
+ attr,
15
+ appId,
16
+ isRequired,
17
+ isIndexed,
18
+ isUnique,
19
+ checkedDataType,
20
+ token,
21
+ }: {
22
+ attr: SchemaAttr;
23
+ appId: string;
24
+ token: string;
25
+ isRequired: boolean;
26
+ isIndexed: boolean;
27
+ isUnique: boolean;
28
+ checkedDataType: CheckedDataType | 'any';
29
+ }) => {
30
+ const [pendingJobs, setPendingJobs] = useState<{
31
+ [jobType in JobConstraintTypes]?: PendingJob;
32
+ }>({});
33
+
34
+ const [runningjobs, setRunningJobs] = useState<{
35
+ [jobType in JobConstraintTypes]?: InstantIndexingJob;
36
+ }>({});
37
+
38
+ const [progress, setProgress] = useState<{ [jobType: string]: number }>({});
39
+
40
+ const explorerProps = useExplorerProps();
41
+
42
+ useEffect(() => {
43
+ // If running jobs, don't update any pending
44
+ const isRunning = Object.values(runningjobs).some(
45
+ (job) => job.job_status !== 'completed' && job.job_status !== 'errored',
46
+ );
47
+ if (isRunning) {
48
+ return;
49
+ }
50
+
51
+ // Pending requirement job
52
+ if (isRequired === attr.isRequired) {
53
+ setPendingJobs((p) => ({ ...p, require: undefined }));
54
+ } else if (isRequired) {
55
+ setPendingJobs((p) => ({ ...p, require: { jobType: 'required' } }));
56
+ } else if (!isRequired) {
57
+ setPendingJobs((p) => ({
58
+ ...p,
59
+ require: { jobType: 'remove-required' },
60
+ }));
61
+ }
62
+
63
+ // Pending index job
64
+ if (isIndexed === attr.isIndex) {
65
+ setPendingJobs((p) => ({ ...p, index: undefined }));
66
+ } else if (isIndexed) {
67
+ setPendingJobs((p) => ({ ...p, index: { jobType: 'index' } }));
68
+ } else if (!isIndexed) {
69
+ setPendingJobs((p) => ({
70
+ ...p,
71
+ index: { jobType: 'remove-index' },
72
+ }));
73
+ }
74
+
75
+ // Pending unique job
76
+ if (isUnique === attr.isUniq) {
77
+ setPendingJobs((p) => ({ ...p, unique: undefined }));
78
+ } else if (isUnique) {
79
+ setPendingJobs((p) => ({ ...p, unique: { jobType: 'unique' } }));
80
+ } else if (!isUnique) {
81
+ setPendingJobs((p) => ({
82
+ ...p,
83
+ unique: { jobType: 'remove-unique' },
84
+ }));
85
+ }
86
+
87
+ // Checked data type
88
+ if (checkedDataType === (attr.checkedDataType || 'any')) {
89
+ setPendingJobs((p) => ({ ...p, type: undefined }));
90
+ } else {
91
+ if (checkedDataType === 'any') {
92
+ setPendingJobs((p) => ({
93
+ ...p,
94
+ type: { jobType: 'remove-data-type' },
95
+ }));
96
+ } else {
97
+ setPendingJobs((p) => ({
98
+ ...p,
99
+ type: { jobType: 'check-data-type', checkedDataType },
100
+ }));
101
+ }
102
+ }
103
+ }, [isRequired, isIndexed, isUnique, checkedDataType, attr, runningjobs]);
104
+
105
+ const [isCreatingJobs, setIsCreatingJobs] = useState(false);
106
+
107
+ const apply = async () => {
108
+ if (isCreatingJobs) return;
109
+
110
+ // Clean up previous errors
111
+ setRunningJobs({});
112
+ setIsCreatingJobs(true);
113
+ Object.entries(pendingJobs).forEach(async ([jobType, pendingJob]) => {
114
+ if (!pendingJob) return;
115
+ const res = await fetch(
116
+ `${explorerProps.apiURI}/dash/apps/${appId}/indexing-jobs`,
117
+ {
118
+ method: 'POST',
119
+ headers: {
120
+ authorization: `Bearer ${token}`,
121
+ 'content-type': 'application/json',
122
+ },
123
+ body: JSON.stringify({
124
+ 'app-id': appId,
125
+ 'attr-id': attr.id,
126
+ 'job-type': pendingJob.jobType,
127
+ 'checked-data-type': pendingJob.checkedDataType,
128
+ }),
129
+ },
130
+ );
131
+ const json = await res.json();
132
+ setRunningJobs((p) => ({ ...p, [jobType]: json.job }));
133
+ setPendingJobs((p) => ({ ...p, [jobType]: undefined }));
134
+ setIsCreatingJobs(false);
135
+ const fetchLoop = jobFetchLoop(
136
+ appId,
137
+ json.job.id,
138
+ token,
139
+ explorerProps.apiURI,
140
+ );
141
+ await fetchLoop.start((updatedJob, error) => {
142
+ if (error) {
143
+ return;
144
+ }
145
+
146
+ if (updatedJob) {
147
+ const workEstimateTotal = updatedJob.work_estimate ?? 50000;
148
+ const workCompletedTotal = updatedJob.work_completed ?? 0;
149
+
150
+ const percent = Math.floor(
151
+ (workCompletedTotal / workEstimateTotal) * 100,
152
+ );
153
+ setProgress((prev) => ({ ...prev, [jobType]: percent }));
154
+
155
+ setRunningJobs((prev) => ({
156
+ ...prev,
157
+ [jobType]: updatedJob,
158
+ }));
159
+ }
160
+ });
161
+ fetchLoop.stop();
162
+ });
163
+ };
164
+
165
+ // Get average of non-zero and non-100 loading values
166
+ const progressPercent = useMemo(() => {
167
+ return (Object.values(progress)
168
+ .filter((p) => p > 0 && p < 100)
169
+ .reduce((a, b) => a + b, 0) /
170
+ Object.values(progress).filter((n) => n > 0 && n < 100).length) as
171
+ | number
172
+ | null;
173
+ }, [progress]);
174
+
175
+ return {
176
+ isPending: Object.values(pendingJobs).filter(Boolean).length > 0,
177
+ progress: progressPercent,
178
+ isRunning: Object.values(runningjobs).some(
179
+ (job) => job.job_status !== 'completed' && job.job_status !== 'errored',
180
+ ),
181
+ pending: pendingJobs,
182
+ running: runningjobs,
183
+ apply,
184
+ };
185
+ };
File without changes
@@ -0,0 +1,24 @@
1
+ import { useLayoutEffect, useRef, useState } from 'react';
2
+
3
+ export function useIsOverflow() {
4
+ const ref = useRef<any>(null);
5
+ const [isOverflow, setIsOverflow] = useState(false);
6
+
7
+ useLayoutEffect(() => {
8
+ const { current } = ref;
9
+
10
+ const trigger = () => {
11
+ const hasOverflow =
12
+ current.scrollWidth > current.clientWidth ||
13
+ current.scrollHeight > current.clientHeight;
14
+
15
+ setIsOverflow(hasOverflow);
16
+ };
17
+
18
+ if (current) {
19
+ trigger();
20
+ }
21
+ }, [ref]);
22
+
23
+ return { ref, isOverflow, setIsOverflow };
24
+ }
@@ -0,0 +1,51 @@
1
+ import { useCallback, useRef, useSyncExternalStore } from 'react';
2
+
3
+ function getSnapshot<T>(k: string): T | undefined {
4
+ if (typeof window == 'undefined') return;
5
+ let v = window.localStorage.getItem(k);
6
+ if (!v) return;
7
+ try {
8
+ return JSON.parse(v);
9
+ } catch (e) {}
10
+ }
11
+
12
+ function setItem<T>(k: string, v: T | undefined) {
13
+ if (typeof window == 'undefined') {
14
+ throw new Error('useLocalStorage/setState needs to run on the client');
15
+ }
16
+ const stringified = JSON.stringify(v);
17
+
18
+ try {
19
+ window.localStorage.setItem(k, stringified);
20
+ } catch (e) {
21
+ console.log("[localStorage] can't set k");
22
+ return;
23
+ }
24
+ // localStorage.setItem does not dispatch events to the current
25
+ window.dispatchEvent(
26
+ new StorageEvent('storage', { key: k, newValue: stringified }),
27
+ );
28
+ }
29
+
30
+ export function useLocalStorage<T>(
31
+ k: string,
32
+ defaultValue: T,
33
+ ): [T, (v: T | undefined) => void] {
34
+ const snapshotRef = useRef<T>(getSnapshot<T>(k) || defaultValue);
35
+ const subscribe = useCallback((cb: Function) => {
36
+ const listener = () => {
37
+ snapshotRef.current = getSnapshot<T>(k) || defaultValue;
38
+ cb();
39
+ };
40
+ window.addEventListener('storage', listener);
41
+ return () => {
42
+ window.removeEventListener('storage', listener);
43
+ };
44
+ }, []);
45
+ const state = useSyncExternalStore<T>(
46
+ subscribe,
47
+ () => snapshotRef.current,
48
+ () => defaultValue,
49
+ );
50
+ return [state, (v: T | undefined) => setItem<T>(k, v)];
51
+ }
@@ -0,0 +1,41 @@
1
+ import { Monaco } from '@monaco-editor/react';
2
+ import { useId } from 'react';
3
+ import { useEffect } from 'react';
4
+
5
+ export function useMonacoJSONSchema(
6
+ path: string,
7
+ monaco?: Monaco,
8
+ schema?: object,
9
+ ) {
10
+ const id = useId();
11
+ useEffect(() => {
12
+ if (!monaco || !schema) return;
13
+ const schemaUri = `http://myserver/myJsonTypeSchema-${id}`;
14
+
15
+ const diagnosticOptions =
16
+ monaco.languages.json.jsonDefaults.diagnosticsOptions;
17
+ const currentSchemas = diagnosticOptions.schemas || [];
18
+
19
+ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
20
+ ...diagnosticOptions,
21
+ schemas: [
22
+ ...currentSchemas,
23
+ {
24
+ uri: schemaUri,
25
+ fileMatch: [path],
26
+ schema: schema,
27
+ },
28
+ ],
29
+ });
30
+
31
+ return () => {
32
+ const currentOptions =
33
+ monaco.languages.json.jsonDefaults.diagnosticsOptions;
34
+ const currentSchemas = currentOptions.schemas || [];
35
+ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
36
+ ...currentOptions,
37
+ schemas: currentSchemas.filter((s) => s.uri !== schemaUri),
38
+ });
39
+ };
40
+ }, [monaco, path, schema, id]);
41
+ }
@@ -0,0 +1,30 @@
1
+ import { init } from '@instantdb/react';
2
+ import { useMemo } from 'react';
3
+
4
+ type InstantReactClient = ReturnType<typeof init>;
5
+ export const useStableDB = ({
6
+ appId,
7
+ apiURI,
8
+ websocketURI,
9
+ adminToken,
10
+ }: {
11
+ appId: string;
12
+ apiURI: string;
13
+ websocketURI: string;
14
+ adminToken?: string;
15
+ }) => {
16
+ const connection = useMemo<InstantReactClient>(
17
+ () =>
18
+ init({
19
+ appId,
20
+ apiURI,
21
+ websocketURI,
22
+ // @ts-ignore
23
+ __adminToken: adminToken,
24
+ disableValidation: true,
25
+ }),
26
+ [appId, apiURI, websocketURI, adminToken],
27
+ );
28
+
29
+ return connection;
30
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,8 @@
1
+ import * as ui from './components/ui';
2
+
3
+ export { Explorer } from './components/explorer/index';
4
+ export type { ExplorerNav } from './components/explorer/index';
5
+ export { StyleMe } from './components/StyleMe';
6
+
7
+ export { ui };
8
+ export * from './components/ui';