├── .commitlintrc
├── .editorconfig
├── .gitignore
├── .npmrc
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── apps
└── web
│ ├── .env.example
│ ├── .eslintignore
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── .prettierignore
│ ├── README.md
│ ├── app
│ ├── components
│ │ ├── NotFound.tsx
│ │ ├── code-highlighter.ts
│ │ ├── copy-to-clipboard.tsx
│ │ ├── data-grid
│ │ │ └── index.tsx
│ │ ├── editor
│ │ │ ├── assets
│ │ │ │ ├── github-dark.json
│ │ │ │ └── github-light.json
│ │ │ ├── components
│ │ │ │ └── monaco.tsx
│ │ │ ├── config.ts
│ │ │ ├── index.tsx
│ │ │ └── utils
│ │ │ │ ├── actions.ts
│ │ │ │ ├── formatter.ts
│ │ │ │ ├── line-selection.tsx
│ │ │ │ ├── monarch.ts
│ │ │ │ ├── share.ts
│ │ │ │ └── theme.ts
│ │ ├── error.tsx
│ │ ├── global-loader.tsx
│ │ ├── lazy-shiki.tsx
│ │ ├── monaco
│ │ │ ├── _inspiration
│ │ │ │ ├── lilac.ts
│ │ │ │ └── readme.md
│ │ │ ├── helpers
│ │ │ │ ├── autocomplete.tsx
│ │ │ │ ├── format.ts
│ │ │ │ ├── formatter.tsx
│ │ │ │ ├── pgsql.ts
│ │ │ │ ├── save.ts
│ │ │ │ ├── selection.ts
│ │ │ │ └── utils.ts
│ │ │ ├── index.tsx
│ │ │ ├── lint
│ │ │ │ └── schema.ts
│ │ │ ├── parts
│ │ │ │ └── command-formatter.ts
│ │ │ ├── readme.md
│ │ │ ├── suggestions
│ │ │ │ └── index.ts
│ │ │ ├── syntax
│ │ │ │ └── index.ts
│ │ │ └── tips
│ │ │ │ └── readme.md
│ │ ├── navbar.tsx
│ │ ├── paginator.tsx
│ │ ├── panel-handle.tsx
│ │ ├── pill.tsx
│ │ ├── plot
│ │ │ ├── components
│ │ │ │ └── settings.tsx
│ │ │ ├── context
│ │ │ │ ├── context.tsx
│ │ │ │ ├── provider.tsx
│ │ │ │ ├── types.ts
│ │ │ │ └── useChart.tsx
│ │ │ └── index.tsx
│ │ ├── table
│ │ │ ├── help.ts
│ │ │ └── table.tsx
│ │ ├── tag.tsx
│ │ ├── tailwind-indicator.tsx
│ │ ├── theme-toggle.tsx
│ │ ├── ui
│ │ │ ├── accordion.tsx
│ │ │ ├── alert-dialog.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── aspect-ratio.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── badge
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── variants.tsx
│ │ │ ├── button
│ │ │ │ ├── button.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── variants.tsx
│ │ │ ├── calendar.tsx
│ │ │ ├── card.tsx
│ │ │ ├── carousel.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── collapsible.tsx
│ │ │ ├── combobox.tsx
│ │ │ ├── command.tsx
│ │ │ ├── context-menu.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── drawer.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── form
│ │ │ │ ├── context.tsx
│ │ │ │ ├── form.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── useFormField.tsx
│ │ │ ├── hover-card.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── link
│ │ │ │ ├── index.ts
│ │ │ │ └── link.tsx
│ │ │ ├── menubar.tsx
│ │ │ ├── multi-select.tsx
│ │ │ ├── navigation-menu
│ │ │ │ ├── index.ts
│ │ │ │ ├── navigation-menu.tsx
│ │ │ │ └── trigger-style.tsx
│ │ │ ├── pagination.tsx
│ │ │ ├── pill
│ │ │ │ ├── index.ts
│ │ │ │ ├── pill.tsx
│ │ │ │ └── variants.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── radio-group.tsx
│ │ │ ├── resizeable.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── select.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── slider.tsx
│ │ │ ├── sonner.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── table.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── toast.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── toggle-group.tsx
│ │ │ ├── toggle
│ │ │ │ ├── index.ts
│ │ │ │ ├── toggle.tsx
│ │ │ │ └── variants.tsx
│ │ │ ├── tooltip.tsx
│ │ │ └── use-toast.ts
│ │ └── virtualized-grid
│ │ │ └── index.tsx
│ ├── constants.client.ts
│ ├── constants.ts
│ ├── context
│ │ ├── db
│ │ │ ├── context.tsx
│ │ │ ├── hooks
│ │ │ │ └── useExport.tsx
│ │ │ ├── provider.tsx
│ │ │ ├── types.ts
│ │ │ └── useDB.tsx
│ │ ├── editor-settings
│ │ │ ├── context.tsx
│ │ │ ├── provider.tsx
│ │ │ ├── types.ts
│ │ │ └── useEditor.tsx
│ │ ├── editor
│ │ │ ├── context.tsx
│ │ │ ├── provider.tsx
│ │ │ ├── types.ts
│ │ │ └── useEditor.tsx
│ │ ├── pagination
│ │ │ ├── context.tsx
│ │ │ ├── provider.tsx
│ │ │ ├── types.ts
│ │ │ └── usePagination.tsx
│ │ ├── panel
│ │ │ ├── context.tsx
│ │ │ ├── provider.tsx
│ │ │ ├── types.ts
│ │ │ └── usePanel.tsx
│ │ ├── query
│ │ │ ├── context.tsx
│ │ │ ├── provider.tsx
│ │ │ ├── types.ts
│ │ │ └── useQuery.tsx
│ │ └── session
│ │ │ ├── context.tsx
│ │ │ ├── data
│ │ │ └── newfile-content.ts
│ │ │ ├── hooks
│ │ │ └── useAddFile.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── provider.tsx
│ │ │ ├── types.ts
│ │ │ ├── useSession.tsx
│ │ │ └── worker.ts
│ ├── entry.client.tsx
│ ├── entry.server.tsx
│ ├── hooks
│ │ ├── use-abortable.tsx
│ │ ├── use-breakpoints.ts
│ │ ├── use-copy-to-clipboard.tsx
│ │ ├── use-enter-submit.tsx
│ │ ├── use-media-query.tsx
│ │ ├── use-mutation-selector.ts
│ │ └── use-prettier-worker.tsx
│ ├── lib
│ │ └── utils.ts
│ ├── modules
│ │ └── duckdb-singleton.ts
│ ├── root.tsx
│ ├── routes
│ │ ├── $.tsx
│ │ ├── _index
│ │ │ ├── components
│ │ │ │ ├── about.tsx
│ │ │ │ ├── editor-panel
│ │ │ │ │ ├── components
│ │ │ │ │ │ └── open-files.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── navbar.tsx
│ │ │ │ ├── playground.tsx
│ │ │ │ ├── query-toolbar.tsx
│ │ │ │ ├── result-viewer
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── chart.tsx
│ │ │ │ │ │ ├── dataset-actions.tsx
│ │ │ │ │ │ ├── empty.tsx
│ │ │ │ │ │ ├── json-viewer
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ └── table.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── settings.tsx
│ │ │ │ └── sidepanel
│ │ │ │ │ ├── components
│ │ │ │ │ ├── data-sources.tsx
│ │ │ │ │ ├── editor-files.tsx
│ │ │ │ │ ├── query-history.tsx
│ │ │ │ │ └── wrapper
│ │ │ │ │ │ ├── context
│ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ ├── provider.tsx
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── useWrapper.tsx
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── index.tsx
│ │ │ ├── route.tsx
│ │ │ └── workers
│ │ │ │ └── is-supported.worker.ts
│ │ ├── action.set-theme.ts
│ │ └── error.tsx
│ ├── sessions.server.ts
│ ├── styles
│ │ ├── dockview.css
│ │ └── globals.css
│ ├── types
│ │ ├── files
│ │ │ ├── code-source.ts
│ │ │ └── dataset.ts
│ │ └── query.ts
│ └── utils
│ │ ├── arrow
│ │ └── helpers.ts
│ │ ├── consume-readable.ts
│ │ ├── debounce.ts
│ │ ├── duckdb
│ │ ├── autocomplete.ts
│ │ ├── helpers
│ │ │ ├── columnMapper.ts
│ │ │ ├── describeColumns.ts
│ │ │ ├── getColumnType.ts
│ │ │ └── insert-snippet.ts
│ │ ├── snippets.ts
│ │ └── tokens.ts
│ │ ├── env.server.ts
│ │ ├── inflect.ts
│ │ ├── nonce-provider.ts
│ │ ├── platform.ts
│ │ ├── sha256.ts
│ │ ├── share.ts
│ │ ├── splitter.tsx
│ │ ├── sql_fmt.ts
│ │ ├── sqlfluff
│ │ ├── fluff.ts
│ │ ├── lint.ts
│ │ └── worker.ts
│ │ ├── text.ts
│ │ ├── timeout.ts
│ │ └── transformer.tsx
│ ├── env.d.ts
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── prettier.config.js
│ ├── public
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── fonts
│ │ ├── GeistVariableVF.woff2
│ │ ├── JetBrainsMono-Italic[wght].woff2
│ │ └── JetBrainsMono[wght].woff2
│ ├── icon512_maskable.png
│ ├── icon512_rounded.png
│ ├── logo-big.webp
│ ├── logo-tiny.webp
│ ├── logo.webp
│ ├── mstile-150x150.png
│ ├── robots.txt
│ ├── safari-pinned-tab.svg
│ ├── screenshot.jpg
│ ├── site.webmanifest
│ ├── sitemap.xml
│ └── spaceman.webp
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ └── vite.config.ts
├── biome.json
├── commitlint.config.cjs
├── ideas
├── README.MD
├── focus-manager.ts
├── online-manager.ts
├── opfs.ts
├── service-worker.ts
├── sqlmesh
│ └── editor.tsx
└── subscribable.ts
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
/.commitlintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@commitlint/config-conventional"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = false
12 | insert_final_newline = false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # Dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # Local env files
9 | .env
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | # Testing
16 | coverage
17 |
18 | # Turbo
19 | .turbo
20 |
21 | # Vercel
22 | .vercel
23 |
24 | # Build Outputs
25 | .next/
26 | out/
27 | build
28 | dist
29 |
30 | # Debug
31 | npm-debug.log*
32 | yarn-debug.log*
33 | yarn-error.log*
34 | pnpm-debug.log*
35 |
36 | # Misc
37 | .DS_Store
38 | *.pem
39 |
40 | .sst
41 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-manager-strict=false
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Matthew Fainman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/apps/web/.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | SESSION_SECRET=your_secret_key
3 |
--------------------------------------------------------------------------------
/apps/web/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | **/**/node_modules
3 | **/**/.next
4 | **/**/public
5 |
6 | # Ignore specific files
7 | dist/
8 | coverage/
9 |
10 | # turbo
11 | .turbo
12 | .vercel
13 | .public
14 | .build
15 |
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | .env
6 | .vercel
7 | .env.*
8 | !.env.example
9 | .DS_Store
10 |
11 | # Sentry Config File
12 | .sentryclirc
13 |
14 | # Sentry Config File
15 | .env.sentry-build-plugin
16 |
--------------------------------------------------------------------------------
/apps/web/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to Remix + Vite!
2 |
3 | 📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features.
4 |
5 | ## Development
6 |
7 | Run the Express server with Vite dev middleware:
8 |
9 | ```shellscript
10 | npm run dev
11 | ```
12 |
13 | ## Deployment
14 |
15 | First, build your app for production:
16 |
17 | ```sh
18 | npm run build
19 | ```
20 |
21 | Then run the app in production mode:
22 |
23 | ```sh
24 | npm start
25 | ```
26 |
27 | Now you'll need to pick a host to deploy it to.
28 |
29 | ### DIY
30 |
31 | If you're familiar with deploying Express applications you should be right at home. Just make sure to deploy the output of `npm run build`
32 |
33 | - `build/server`
34 | - `build/client`
35 |
--------------------------------------------------------------------------------
/apps/web/app/components/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@remix-run/react";
2 | import { Suspense } from "react";
3 |
4 | export default function NotFound() {
5 | return (
6 |
7 |
8 |
9 | 404
10 |
11 |
12 | Page not found
13 |
14 |
15 | {`Sorry, we couldn't find the page you're looking for.`}
16 |
17 |
18 |
22 | ← Back to home
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/apps/web/app/components/code-highlighter.ts:
--------------------------------------------------------------------------------
1 | import type { HighlighterCore } from "shiki/core";
2 | import { getHighlighterCore } from "shiki/core";
3 | import getWasm from "shiki/wasm";
4 |
5 | let shiki: HighlighterCore | undefined;
6 |
7 | export const getHighlighter = async () => {
8 | if (shiki) return shiki;
9 |
10 | shiki = await getHighlighterCore({
11 | langs: [import("shiki/langs/sql.mjs"), import("shiki/langs/json.mjs")],
12 | loadWasm: getWasm,
13 | themes: [
14 | import("shiki/themes/github-light.mjs"),
15 | import("shiki/themes/aurora-x.mjs"),
16 | import("shiki/themes/vitesse-dark.mjs"),
17 | import("shiki/themes/vitesse-light.mjs"),
18 | ],
19 | });
20 |
21 | return shiki;
22 | };
23 |
--------------------------------------------------------------------------------
/apps/web/app/components/data-grid/index.tsx:
--------------------------------------------------------------------------------
1 | import { format } from "date-fns";
2 | import { useMemo } from "react";
3 | import {
4 | Table,
5 | TableBody,
6 | TableCell,
7 | TableHead,
8 | TableHeader,
9 | TableRow,
10 | } from "~/components/ui/table";
11 | import { usePagination } from "~/context/pagination/usePagination";
12 | import { type QueryResponse } from "~/types/query";
13 | import { getArrowTableSchema } from "~/utils/arrow/helpers";
14 | import { getColumnType } from "~/utils/duckdb/helpers/getColumnType";
15 |
16 | export default function DataGrid(props: QueryResponse) {
17 | const { limit, offset } = usePagination();
18 | const { schema, rows } = useMemo(() => {
19 | if (!props.table || props.table.numRows === 0)
20 | return { schema: [], rows: [] };
21 | const rows = props.table
22 | .slice(offset, offset + limit)
23 | .toArray()
24 | .map((row) => row.toJSON());
25 | const schema = getArrowTableSchema(props.table);
26 | return { schema, rows };
27 | }, [props, limit, offset]);
28 |
29 | return (
30 |
31 |
32 |
33 | {schema.map((column) => {
34 | return {column.name};
35 | })}
36 |
37 |
38 |
39 |
40 | {rows.map((row, i) => (
41 |
42 | {Object.entries(row).map(([column, value]) => {
43 | const type =
44 | schema.find((col) => col.name === column)?.type ?? "other";
45 |
46 | const coercedType = getColumnType(type);
47 |
48 | const Node = dynamicTypeViewer({
49 | type: coercedType,
50 | value,
51 | });
52 |
53 | return {Node};
54 | })}
55 |
56 | ))}
57 |
58 |
59 | );
60 | }
61 |
62 | type DynamicTypeViewerProps = {
63 | type:
64 | | "bigint"
65 | | "number"
66 | | "integer"
67 | | "boolean"
68 | | "date"
69 | | "string"
70 | | "other";
71 | value: unknown;
72 | };
73 |
74 | function dynamicTypeViewer(props: DynamicTypeViewerProps) {
75 | const { type, value } = props;
76 |
77 | switch (type) {
78 | case "date": {
79 | const date = format(new Date(value as string), "PPpp");
80 | return date;
81 | }
82 | case "string": {
83 | return value as string;
84 | }
85 | case "bigint": {
86 | return (value as bigint).toString();
87 | }
88 | case "boolean": {
89 | return `${value}` as string;
90 | }
91 | case "other": {
92 | return JSON.stringify(value);
93 | }
94 | case "integer":
95 | case "number": {
96 | if (isNaN(value as number)) return "";
97 | // round to 2 decimal places
98 | const formatter = new Intl.NumberFormat("en-UK", {
99 | maximumFractionDigits: 2,
100 | });
101 | return (
102 | {formatter.format(value as number)}
103 | );
104 | }
105 | default:
106 | return "";
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/apps/web/app/components/editor/components/monaco.tsx:
--------------------------------------------------------------------------------
1 | // import { Editor, type OnChange, type OnMount } from "@monaco-editor/react";
2 | // import { useCallback } from "react";
3 |
4 | // type IProps = {
5 | // defaultValue: string;
6 | // onChange: OnChange;
7 | // onSelect: (selection: { hi: number; lo: number }) => void;
8 | // };
9 |
10 | // export const Input: React.FC = (props) => {
11 | // const { defaultValue, onChange, onSelect } = props;
12 |
13 | // const onMount = useCallback(
14 | // (editor) => {
15 | // editor.createContextKey("share_available", navigator.share !== undefined);
16 | // const module = editor.getModel()!;
17 |
18 | // const { dispose } = editor.onDidChangeCursorSelection((e) => {
19 | // const start = module.getOffsetAt(e.selection.getStartPosition());
20 | // const end = module.getOffsetAt(e.selection.getEndPosition());
21 |
22 | // const lo = start + 1;
23 | // const hi = end + 1;
24 |
25 | // onSelect({ lo, hi });
26 | // });
27 |
28 | // return dispose;
29 | // },
30 | // [onSelect],
31 | // );
32 |
33 | // return (
34 | //
41 | // );
42 | // };
43 |
--------------------------------------------------------------------------------
/apps/web/app/components/editor/config.ts:
--------------------------------------------------------------------------------
1 | // import { loader } from "@monaco-editor/react";
2 | // import {
3 | // copy_as_markdown,
4 | // copy_as_url,
5 | // open_issue,
6 | // share,
7 | // } from "./utils/actions";
8 |
9 | // loader.init().then((monaco) => {
10 | // copy_as_url(monaco);
11 | // copy_as_markdown(monaco);
12 | // open_issue(monaco);
13 | // share(monaco);
14 | // monaco.editor.addKeybindingRule({
15 | // keybinding:
16 | // monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyP,
17 | // command: "editor.action.quickCommand",
18 | // });
19 | // });
20 |
--------------------------------------------------------------------------------
/apps/web/app/components/editor/index.tsx:
--------------------------------------------------------------------------------
1 | // import lz from "lz-string";
2 | // import { Suspense, useCallback, useEffect, useState } from "react";
3 | // import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
4 | // import { Textarea } from "~/components/ui/textarea";
5 | // import { localStore } from "./utils/share";
6 |
7 | // const getStoredCode = (location: Location) => {
8 | // let init_code = "";
9 | // if (location.hash.startsWith("#code/")) {
10 | // const code = location.hash.replace("#code/", "").trim();
11 | // localStore.code = init_code = lz.decompressFromEncodedURIComponent(code);
12 | // } else {
13 | // init_code = localStore.code;
14 | // }
15 |
16 | // return init_code;
17 | // };
18 |
19 | // export default function CodeEditor() {
20 | // const [code, setCode] = useState("");
21 | // const [selection, setSelection] = useState({ lo: 0, hi: 0 });
22 |
23 | // useEffect(() => {
24 | // setCode(getStoredCode(window.location));
25 | // }, []);
26 |
27 | // const onChange = useCallback((value?: string) => {
28 | // if (value !== undefined) {
29 | // setCode(value);
30 | // }
31 | // }, []);
32 |
33 | // return (
34 | //
35 | //
39 | //
44 | //
49 | //
50 | //
51 | // {code}
52 | //
53 | //
54 | //
55 | // );
56 | // }
57 |
--------------------------------------------------------------------------------
/apps/web/app/components/editor/utils/actions.ts:
--------------------------------------------------------------------------------
1 | // // https://github.com/magic-akari/swc-ast-viewer/blob/main/src/monaco/action.ts
2 | // import type { Monaco } from "@monaco-editor/react";
3 | // import { reportIssue, shareMarkdown, shareURL } from "./share";
4 |
5 | // export function copy_as_url(monaco: Monaco) {
6 | // monaco.editor.addEditorAction({
7 | // id: "swc-ast-viewer.copy-as-url",
8 | // label: "Copy as URL",
9 | // precondition: "!editorReadonly",
10 | // contextMenuOrder: 5,
11 | // contextMenuGroupId: "9_cutcopypaste",
12 | // run(editor) {
13 | // const code = editor.getValue();
14 | // const result = shareURL(code);
15 | // navigator.clipboard.writeText(result);
16 | // },
17 | // });
18 | // }
19 |
20 | // export function copy_as_markdown(monaco: Monaco) {
21 | // monaco.editor.addEditorAction({
22 | // id: "swc-ast-viewer.copy-as-markdown",
23 | // label: "Copy as Markdown Link",
24 | // precondition: "!editorReadonly",
25 | // contextMenuOrder: 5.1,
26 | // contextMenuGroupId: "9_cutcopypaste",
27 | // run(editor) {
28 | // const code = editor.getValue();
29 | // const result = shareMarkdown(code);
30 | // navigator.clipboard.writeText(result);
31 | // },
32 | // });
33 | // }
34 |
35 | // export function open_issue(monaco: Monaco) {
36 | // monaco.editor.addEditorAction({
37 | // id: "swc-ast-viewer.open-issue",
38 | // label: "Open Issue in SWC Repository",
39 | // precondition: "!editorReadonly",
40 | // contextMenuOrder: 3,
41 | // contextMenuGroupId: "issue",
42 | // run(editor) {
43 | // const code = editor.getValue();
44 | // const result = reportIssue(code);
45 | // window.open(result);
46 | // },
47 | // });
48 | // }
49 |
50 | // export function share(monaco: Monaco) {
51 | // monaco.editor.addEditorAction({
52 | // id: "swc-ast-viewer.share",
53 | // label: "Share",
54 | // precondition: "!editorReadonly && share_available",
55 | // contextMenuOrder: 4,
56 | // contextMenuGroupId: "share",
57 | // run(editor) {
58 | // const code = editor.getValue();
59 | // const url = shareURL(code);
60 | // navigator.share({
61 | // title: "SWC AST Viewer",
62 | // text: code,
63 | // url,
64 | // });
65 | // },
66 | // });
67 | // }
68 |
--------------------------------------------------------------------------------
/apps/web/app/components/editor/utils/formatter.ts:
--------------------------------------------------------------------------------
1 | // // https://github.com/magic-akari/swc-ast-viewer/blob/main/src/monaco/fmt.ts
2 | // import type { Monaco } from "@monaco-editor/react";
3 | // import init, { format } from "@wasm-fmt/sql_fmt/vite";
4 |
5 | // init();
6 |
7 | // export function config_fmt(monaco: Monaco) {
8 | // monaco.languages.registerDocumentFormattingEditProvider(
9 | // ["sql", "mysql", "pgsql", "sqlite", "mssql", "plsql", "tsql"],
10 | // {
11 | // provideDocumentFormattingEdits(model, options) {
12 | // const text = model.getValue();
13 | // const indent_style = options.insertSpaces ? "space" : "tab";
14 | // const indent_width = options.tabSize;
15 |
16 | // try {
17 | // const formatted = format(text, model.uri.path, {
18 | // indent_style,
19 | // indent_width,
20 | // });
21 |
22 | // return [
23 | // {
24 | // range: model.getFullModelRange(),
25 | // text: formatted,
26 | // },
27 | // ];
28 | // } catch (error) {
29 | // console.error(error);
30 | // return [];
31 | // }
32 | // },
33 | // },
34 | // );
35 | // }
36 |
--------------------------------------------------------------------------------
/apps/web/app/components/editor/utils/line-selection.tsx:
--------------------------------------------------------------------------------
1 | // import { Range, type editor } from "monaco-editor/esm/vs/editor/editor.api";
2 |
3 | // /**
4 | // * Selects the whole line when clicking on the line number.
5 | // *
6 | // * @source https://github.com/opensumi/core/blob/ccd710f47f4ef56ae047de51be5844c9749c4afd/packages/monaco/src/browser/monaco.service.ts#L98C3-L118C4
7 | // */
8 | // export function lineSelector(editor: editor.ICodeEditor) {
9 | // return editor.onMouseDown((e) => {
10 | // // if click on line number, select the whole line
11 | // if (e.target.type === 6) {
12 | // const lineNumber =
13 | // e.target.position?.lineNumber || e.target.range?.startLineNumber;
14 | // if (!lineNumber) {
15 | // return;
16 | // }
17 |
18 | // editor.setSelection(
19 | // new Range(
20 | // lineNumber,
21 | // e.target.range?.startColumn || e.target.position?.column || 0,
22 | // lineNumber + 1,
23 | // e.target.range?.startColumn || e.target.position?.column || 0,
24 | // ),
25 | // );
26 | // }
27 | // });
28 | // }
29 |
--------------------------------------------------------------------------------
/apps/web/app/components/editor/utils/share.ts:
--------------------------------------------------------------------------------
1 | // // https://github.com/magic-akari/swc-ast-viewer/blob/main/src/share.ts
2 | // import lz from "lz-string";
3 |
4 | // export const localStore = {
5 | // get code(): string {
6 | // return localStorage.getItem("code") || "";
7 | // },
8 | // set code(code: string) {
9 | // localStorage.setItem("code", code);
10 | // },
11 | // };
12 |
13 | // export function shareURL(code: string): string {
14 | // const url = new URL(location.href);
15 | // if (code) {
16 | // url.hash = "code/" + lz.compressToEncodedURIComponent(code);
17 | // }
18 |
19 | // return url.toString();
20 | // }
21 |
22 | // export function shareMarkdown(code: string): string {
23 | // const url = shareURL(code);
24 | // return `[SWC AST Viewer](${url})`;
25 | // }
26 |
27 | // export function reportIssue(code: string): string {
28 | // const reportUrl = new URL(
29 | // `https://github.com/mattf96s/QuackDB/issues/new?labels=C-bug&template=bug_report.yml`,
30 | // );
31 |
32 | // const link = shareMarkdown(code);
33 |
34 | // reportUrl.searchParams.set("code", code);
35 | // reportUrl.searchParams.set("repro-link", link);
36 |
37 | // return reportUrl.toString();
38 | // }
39 |
--------------------------------------------------------------------------------
/apps/web/app/components/editor/utils/theme.ts:
--------------------------------------------------------------------------------
1 | // // https://github.com/magic-akari/swc-ast-viewer/blob/main/src/monaco/theme.ts
2 | // import type { Monaco } from "@monaco-editor/react";
3 | // import type { editor } from "monaco-editor";
4 | // import github_dark from "../assets/github-dark.json" with { type: "json" };
5 | // import github_light from "../assets/github-light.json" with { type: "json" };
6 |
7 | // export function config_theme(monaco: Monaco) {
8 | // monaco.editor.defineTheme(
9 | // "github-light",
10 | // github_light as editor.IStandaloneThemeData,
11 | // );
12 | // monaco.editor.defineTheme(
13 | // "github-dark",
14 | // github_dark as editor.IStandaloneThemeData,
15 | // );
16 |
17 | // const darkMatch = window.matchMedia?.("(prefers-color-scheme: dark)");
18 |
19 | // set_theme(darkMatch?.matches, monaco);
20 |
21 | // darkMatch?.addEventListener("change", (ev) => {
22 | // set_theme(ev.matches, monaco);
23 | // });
24 | // }
25 |
26 | // function set_theme(is_dark: boolean, monaco: Monaco) {
27 | // if (is_dark) {
28 | // monaco.editor.setTheme("github-dark");
29 | // } else {
30 | // monaco.editor.setTheme("github-light");
31 | // }
32 | // }
33 |
--------------------------------------------------------------------------------
/apps/web/app/components/error.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
2 |
3 | import { motion } from "framer-motion";
4 | import { AlertOctagon, ChevronDown } from "lucide-react";
5 | import { useState } from "react";
6 | import { useTheme } from "remix-themes";
7 | import { useQuery } from "~/context/query/useQuery";
8 | import { cn } from "~/lib/utils";
9 | import { ScrollArea } from "./ui/scroll-area";
10 |
11 | const prettify = (str: string) => {
12 | try {
13 | // remove newlines
14 | let pretty = str.replaceAll(/\n/g, "");
15 |
16 | pretty = JSON.stringify(JSON.parse(pretty), null, "\t");
17 |
18 | return pretty;
19 | } catch (e) {
20 | return str;
21 | }
22 | };
23 |
24 | export default function ErrorNotification(props: { error: string }) {
25 | const [isOpen, setIsOpen] = useState(false);
26 | const { meta } = useQuery();
27 | const [theme] = useTheme();
28 | const isDark = theme === "dark";
29 |
30 | const error = prettify(props.error);
31 |
32 | return (
33 |
40 |
41 |
42 |
46 |
47 |
48 | {error}
49 |
50 |
51 |
52 |
61 |
62 |
63 | {isOpen && (
64 |
65 | {Object.entries(meta ?? {}).map(([key, value]) => (
66 |
70 | {key}
71 |
72 | {typeof value === "string"
73 | ? prettify(value)
74 | : JSON.stringify(value, null, 2)}
75 |
76 |
77 | ))}
78 |
79 | )}
80 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/apps/web/app/components/global-loader.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigation } from "@remix-run/react";
2 |
3 | import { cn } from "~/lib/utils";
4 |
5 | /**
6 | * GitHub like global pending indicator.
7 | * @source https://github.com/jacob-ebey/remix-shadcn/blob/main/app/components/global-pending-indicator.tsx
8 | */
9 | export default function GlobalPendingIndicator() {
10 | const navigation = useNavigation();
11 | const pending = navigation.state !== "idle";
12 |
13 | return (
14 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/app/components/lazy-shiki.tsx:
--------------------------------------------------------------------------------
1 | import { Await } from "@remix-run/react";
2 | import DOMPurify from "dompurify";
3 | import { Suspense } from "react";
4 | import { useTheme } from "remix-themes";
5 | import { type CodeToHastOptionsCommon, type HighlighterCore } from "shiki/core";
6 | import { getHighlighter } from "~/components/code-highlighter";
7 |
8 | let shiki: HighlighterCore | undefined;
9 |
10 | type CreateHighlighterProps = {
11 | text: string;
12 | lang: CodeToHastOptionsCommon["lang"];
13 | };
14 |
15 | const createHighlighter = async (
16 | props: CreateHighlighterProps & { isDark: boolean },
17 | ) => {
18 | try {
19 | if (!shiki) shiki = await getHighlighter();
20 |
21 | const html = shiki.codeToHtml(props.text, {
22 | lang: props.lang,
23 | theme: props.isDark ? "vitesse-dark" : "github-light",
24 | });
25 | if (!html) throw new Error("Failed to create highlighter");
26 | return DOMPurify.sanitize(html);
27 | } catch (e) {
28 | console.error("Failed to create highlighter: ", e);
29 | return "";
30 | }
31 | };
32 |
33 | export default function Highlighter(props: CreateHighlighterProps) {
34 | const [theme] = useTheme();
35 | const isDark = theme === "dark";
36 |
37 | return (
38 |
39 |
43 |
44 | );
45 | }
46 |
47 | function HighlightContent(
48 | props: CreateHighlighterProps & { isDark: boolean },
49 | ): JSX.Element {
50 | const html = createHighlighter(props);
51 |
52 | return (
53 |
54 |
55 | {(p) => (
56 |
61 | )}
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/apps/web/app/components/monaco/_inspiration/readme.md:
--------------------------------------------------------------------------------
1 | # Good examples from other projects
2 |
3 | ## [VSCode Async](https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts#L24)
4 |
5 | So good
6 |
--------------------------------------------------------------------------------
/apps/web/app/components/monaco/helpers/autocomplete.tsx:
--------------------------------------------------------------------------------
1 | import { matchSorter } from "match-sorter";
2 | import { languages } from "monaco-editor";
3 | import { snippets } from "~/utils/duckdb/snippets";
4 | import { language } from "./pgsql";
5 |
6 | type PartialMonacoCompletionItem = Pick<
7 | languages.CompletionItem,
8 | "label" | "kind" | "insertText" | "detail"
9 | >;
10 |
11 | const getDefaultSuggestions = (): PartialMonacoCompletionItem[] => {
12 | const keywords: PartialMonacoCompletionItem[] = (
13 | language.keywords as string[]
14 | ).map((keyword) => ({
15 | label: keyword,
16 | kind: languages.CompletionItemKind.Keyword,
17 | insertText: keyword,
18 | }));
19 |
20 | const fns: PartialMonacoCompletionItem[] = (
21 | language.builtinFunctions as string[]
22 | ).map((fn) => ({
23 | label: fn,
24 | kind: languages.CompletionItemKind.Function,
25 | insertText: fn,
26 | }));
27 |
28 | const operators: PartialMonacoCompletionItem[] = (
29 | language.operators as string[]
30 | ).map((op) => ({
31 | label: op,
32 | kind: languages.CompletionItemKind.Operator,
33 | insertText: op,
34 | }));
35 |
36 | const duckdbSnippets: PartialMonacoCompletionItem[] = snippets.map(
37 | (snippet) => ({
38 | label: snippet.name,
39 | kind: languages.CompletionItemKind.Snippet,
40 | insertText: snippet.code,
41 | detail: snippet.description,
42 | }),
43 | );
44 |
45 | return [...duckdbSnippets, ...keywords, ...fns, ...operators];
46 | };
47 |
48 | const cache = new Map();
49 |
50 | const getSuggestions = () => {
51 | const cached = cache.get(language.id);
52 | if (cached) return cached;
53 | const suggestions = getDefaultSuggestions();
54 | cache.set(language.id, suggestions);
55 | return suggestions;
56 | };
57 |
58 | export const autocompleter = (term: string) => {
59 | const allCandidates = getSuggestions();
60 |
61 | const suggestions = matchSorter(allCandidates, `${term}`, {
62 | keys: ["label"],
63 | });
64 |
65 | return {
66 | suggestions,
67 | incomplete: true,
68 | };
69 | };
70 |
--------------------------------------------------------------------------------
/apps/web/app/components/monaco/helpers/format.ts:
--------------------------------------------------------------------------------
1 | // import type { editor, Position } from "monaco-editor";
2 |
3 | // const computeOffset = (code: string, pos: Position) => {
4 | // let line = 1;
5 | // let col = 1;
6 | // let offset = 0;
7 | // while (offset < code.length) {
8 | // if (line === pos.lineNumber && col === pos.column) return offset;
9 | // if (code[offset] === "\n") line++, (col = 1);
10 | // else col++;
11 | // offset++;
12 | // }
13 | // return -1;
14 | // };
15 | // /**
16 | // * Source: https://github.com/lukejacksonn/monacode/blob/master/index.js
17 | // */
18 | // export const formatFiles =
19 | // (editor: editor.IStandaloneCodeEditor) => (e: Event) => {
20 | // e.preventDefault();
21 | // const val = editor.getValue();
22 | // const pos = editor.getPosition();
23 |
24 | // const prettyVal = prettier.formatWithCursor(val, {
25 | // parser: "babel",
26 | // plugins: prettierBabel,
27 | // cursorOffset: computeOffset(val, pos),
28 | // });
29 |
30 | // editor.executeEdits("prettier", [
31 | // {
32 | // identifier: "delete",
33 | // range: editor.getModel().getFullModelRange(),
34 | // text: "",
35 | // forceMoveMarkers: true,
36 | // },
37 | // ]);
38 |
39 | // editor.executeEdits("prettier", [
40 | // {
41 | // identifier: "insert",
42 | // range: new monaco.Range(1, 1, 1, 1),
43 | // text: prettyVal.formatted,
44 | // forceMoveMarkers: true,
45 | // },
46 | // ]);
47 |
48 | // editor.setSelection(new monaco.Range(0, 0, 0, 0));
49 | // editor.setPosition(
50 | // computePosition(prettyVal.formatted, prettyVal.cursorOffset),
51 | // );
52 | // };
53 |
--------------------------------------------------------------------------------
/apps/web/app/components/monaco/helpers/formatter.tsx:
--------------------------------------------------------------------------------
1 | // // https://github.com/TypeCellOS/TypeCell/blob/staging/packages/frame/src/runtime/editor/prettier/index.ts
2 | // import type * as monaco from "monaco-editor";
3 | // import { diffToMonacoTextEdits } from "./diffToMonacoTextEdits";
4 |
5 | // // TODO: move prettier to shared webworker or host frame?
6 | // export function setupPrettier(monacoInstance: typeof monaco) {
7 | // monacoInstance.languages.registerDocumentFormattingEditProvider(
8 | // "typescript",
9 | // {
10 | // async provideDocumentFormattingEdits(model, options, token) {
11 | // try {
12 | // const prettier = await import("prettier/standalone");
13 | // const parserTypescript = await import("prettier/plugins/typescript");
14 | // const esTree = await import("prettier/plugins/estree");
15 |
16 | // const newText = await prettier.format(model.getValue(), {
17 | // parser: "typescript",
18 | // plugins: [parserTypescript, esTree.default],
19 | // tabWidth: 2,
20 | // printWidth: 80,
21 | // jsxBracketSameLine: true,
22 | // });
23 |
24 | // const ret = diffToMonacoTextEdits(
25 | // model,
26 | // newText.substring(0, newText.length - 1) // disable last \n added by prettier
27 | // );
28 | // return ret;
29 | // } catch (e) {
30 | // console.warn("error while formatting ts code (prettier)", e);
31 | // return [];
32 | // }
33 | // },
34 | // }
35 | // );
36 |
37 | // monacoInstance.languages.registerDocumentFormattingEditProvider("css", {
38 | // async provideDocumentFormattingEdits(model, options, token) {
39 | // const prettier = await import("prettier/standalone");
40 | // const parserCSS = await import("prettier/plugins/postcss");
41 | // try {
42 | // const newText = await prettier.format(model.getValue(), {
43 | // parser: "css",
44 | // plugins: [parserCSS],
45 | // tabWidth: 2,
46 | // printWidth: 80,
47 | // });
48 |
49 | // const ret = diffToMonacoTextEdits(model, newText);
50 | // return ret;
51 | // } catch (e) {
52 | // console.warn("error while formatting css code (prettier)", e);
53 | // return [];
54 | // }
55 | // },
56 | // });
57 | // }
58 |
--------------------------------------------------------------------------------
/apps/web/app/components/monaco/helpers/save.ts:
--------------------------------------------------------------------------------
1 | import type { editor } from "monaco-editor";
2 |
3 | /**
4 | * Save the contents of the Monaco editor to local storage and restore it on page load.
5 | *
6 | * Should be used as a callback for the `onDidCreateEditor` event.
7 | *
8 | * @source https://github.com/rhashimoto/preview/blob/master/demo/demo.js
9 | */
10 | export const onSaveToLocalStorage =
11 | (SQL_KEY: string) => (editor: editor.IStandaloneCodeEditor) => {
12 | // Persist editor content across page loads.
13 | let change: NodeJS.Timeout;
14 | const disposable = editor.onDidChangeModelContent(function () {
15 | clearTimeout(change);
16 | change = setTimeout(function () {
17 | localStorage.setItem(SQL_KEY, editor.getValue());
18 | }, 1000);
19 | });
20 |
21 | editor.setValue(localStorage.getItem(SQL_KEY) ?? "MONACO_EDITOR_CONTENT");
22 |
23 | return disposable;
24 | };
25 |
--------------------------------------------------------------------------------
/apps/web/app/components/monaco/helpers/utils.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/windmill-labs/windmill/blob/05a1e19b5e3c2e26d858e5024bbc3494da0abf4c/frontend/src/lib/components/Editor.svelte#L212C9-L231C3
2 | // function setCode(ncode: string, noHistory: boolean = false): void {
3 | // code = ncode
4 | // if (noHistory) {
5 | // editor?.setValue(ncode)
6 | // } else {
7 | // if (editor?.getModel()) {
8 | // // editor.setValue(ncode)
9 | // editor.pushUndoStop()
10 |
11 | // editor.executeEdits('set', [
12 | // {
13 | // range: editor.getModel()!.getFullModelRange(), // full range
14 | // text: ncode
15 | // }
16 | // ])
17 |
18 | // editor.pushUndoStop()
19 | // }
20 | // }
21 | // }
22 |
23 | // https://github.com/windmill-labs/windmill/blob/05a1e19b5e3c2e26d858e5024bbc3494da0abf4c/frontend/src/lib/components/Editor.svelte#L233C9-L253C1
24 | // function append(code): void {
25 | // if (editor) {
26 | // const lineCount = editor.getModel()?.getLineCount() || 0
27 | // const lastLineLength = editor.getModel()?.getLineLength(lineCount) || 0
28 | // const range: IRange = {
29 | // startLineNumber: lineCount,
30 | // startColumn: lastLineLength + 1,
31 | // endLineNumber: lineCount,
32 | // endColumn: lastLineLength + 1
33 | // }
34 | // editor.executeEdits('append', [
35 | // {
36 | // range,
37 | // text: code,
38 | // forceMoveMarkers: true
39 | // }
40 | // ])
41 | // editor.revealLine(lineCount)
42 | // }
43 | // }
44 |
--------------------------------------------------------------------------------
/apps/web/app/components/monaco/parts/command-formatter.ts:
--------------------------------------------------------------------------------
1 | import { KeyCode, KeyMod, type editor } from "monaco-editor";
2 |
3 | /**
4 | * Add an SQL command formatter to the editor.
5 | *
6 | * Source https://github.com/axiomhq/monaco-kusto/blob/master/package/src/commandFormatter.ts
7 | */
8 | export default class SQLCommandFormatter {
9 | private actionAdded: boolean = false;
10 |
11 | constructor(private editor: editor.IStandaloneCodeEditor) {
12 | // selection also represents no selection - for example the event gets triggered when moving cursor from point
13 | // a to point b. in the case start position will equal end position.
14 | editor.onDidChangeCursorSelection((_changeEvent) => {
15 | const languageId = this.editor.getModel()?.getLanguageId();
16 | if (!languageId) return;
17 | if (!["sql", "pgsql"].includes(languageId)) {
18 | return;
19 | }
20 | // Theoretically you would expect this code to run only once in onDidCreateEditor.
21 | // Turns out that onDidCreateEditor is fired before the IStandaloneEditor is completely created (it is emmited by
22 | // the super ctor before the child ctor was able to fully run).
23 | // Thus we don't have a key binding provided yet when onDidCreateEditor is run, which is essential to call addAction.
24 | // By adding the action here in onDidChangeCursorSelection we're making sure that the editor has a key binding provider,
25 | // and we just need to make sure that this happens only once.
26 | if (!this.actionAdded) {
27 | editor.addAction({
28 | id: "editor.action.sql.formatCurrentCommand",
29 | label: "Format Command Under Cursor",
30 | keybindings: [
31 | KeyMod.chord(
32 | KeyMod.CtrlCmd | KeyCode.KeyK,
33 | KeyMod.CtrlCmd | KeyCode.KeyF,
34 | ),
35 | ],
36 | run: (_ed: editor.IStandaloneCodeEditor) => {
37 | editor.trigger(
38 | "SQLCommandFormatter",
39 | "editor.action.formatSelection",
40 | null,
41 | );
42 | },
43 | contextMenuGroupId: "1_modification",
44 | });
45 | this.actionAdded = true;
46 | }
47 | });
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/apps/web/app/components/monaco/readme.md:
--------------------------------------------------------------------------------
1 | # Links
2 |
3 | See
4 |
5 | Copilot:
6 |
7 | Tailwind completions:
8 |
9 | Open handler registry:
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/apps/web/app/components/monaco/tips/readme.md:
--------------------------------------------------------------------------------
1 | [duckdb SQL select result to JSON](https://stackoverflow.com/questions/77757166/duckdb-sql-select-result-to-json)
2 |
3 | SELECT {city: list(city), temp_hi: list(temp_hi)}::JSON AS j FROM weather;
4 | SELECT struct_pack(city := list(city), temp_hi := list(temp_hi))::JSON AS j FROM weather;
5 |
6 | ```json
7 | ┌───────────────────────────────────────────────────────┐
8 | │ j │
9 | │ json │
10 | ├───────────────────────────────────────────────────────┤
11 | │ {"city":["San Francisco","Vienna"],"temp_hi":[50,35]} │
12 | └───────────────────────────────────────────────────────┘
13 | ```
14 |
--------------------------------------------------------------------------------
/apps/web/app/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@remix-run/react";
2 | import { Terminal } from "lucide-react";
3 | import { Suspense } from "react";
4 |
5 | export default function NavBar(props: { children: React.ReactNode }) {
6 | return (
7 |
8 |
9 |
10 |
QuackDB
11 |
15 |
16 |
17 |
18 | {props.children}
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | function HomeIcon() {
26 | return (
27 |
32 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/apps/web/app/components/panel-handle.tsx:
--------------------------------------------------------------------------------
1 | import { DragHandleDots2Icon } from "@radix-ui/react-icons";
2 | import { PanelResizeHandle } from "react-resizable-panels";
3 | import { cn } from "~/lib/utils";
4 |
5 | /**
6 | * Panel resize handle component.
7 | *
8 | * @component
9 | *
10 | * @example
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | */
21 | export default function PanelHandle() {
22 | return (
23 | div]:rotate-90",
26 | )}
27 | >
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/apps/web/app/components/pill.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "~/components/ui/badge";
2 | import { cn } from "~/lib/utils";
3 |
4 | export default function Pill(props: { children: React.ReactNode }) {
5 | return (
6 |
10 | {props.children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/apps/web/app/components/plot/components/settings.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type * as Plot from "@observablehq/plot";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardHeader,
9 | CardTitle,
10 | } from "~/components/ui/card";
11 | import { Label } from "~/components/ui/label";
12 | import {
13 | Select,
14 | SelectContent,
15 | SelectItem,
16 | SelectTrigger,
17 | SelectValue,
18 | } from "~/components/ui/select";
19 | import { useChart } from "../context/useChart";
20 |
21 | // Don't seem to have any effect on Plot.auto mark.
22 | const schemaOptions: {
23 | value: NonNullable;
24 | label: string;
25 | }[] = [
26 | {
27 | label: "Category10",
28 | value: "category10",
29 | },
30 | {
31 | label: "Accent",
32 | value: "accent",
33 | },
34 | {
35 | label: "Dark2",
36 | value: "dark2",
37 | },
38 | {
39 | label: "Paired",
40 | value: "paired",
41 | },
42 | {
43 | label: "Pastel1",
44 | value: "pastel1",
45 | },
46 | {
47 | label: "Pastel2",
48 | value: "pastel2",
49 | },
50 | {
51 | label: "Set1",
52 | value: "set1",
53 | },
54 | {
55 | label: "Set2",
56 | value: "set2",
57 | },
58 | {
59 | label: "Set3",
60 | value: "set3",
61 | },
62 | {
63 | label: "Tableau10",
64 | value: "tableau10",
65 | },
66 | ];
67 |
68 | export default function PlotSettings() {
69 | const { scheme, _dispatch } = useChart();
70 |
71 | return (
72 |
73 |
74 |
75 | Chart Settings
76 | Adjust the settings for your chart.
77 |
78 |
79 |
80 |
81 |
82 |
108 |
109 |
110 |
111 |
112 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/apps/web/app/components/plot/context/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import type { ChartState } from "./types";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 | export const ChartContext = createContext(undefined);
6 |
--------------------------------------------------------------------------------
/apps/web/app/components/plot/context/types.ts:
--------------------------------------------------------------------------------
1 | import type * as Plot from "@observablehq/plot";
2 | import type { ResultColumn } from "~/utils/arrow/helpers";
3 |
4 | export type ChartState = Pick<
5 | Plot.AutoOptions,
6 | "x" | "y" | "color" | "mark"
7 | > & {
8 | data: Plot.Data;
9 | columns: ResultColumn[];
10 | scheme: Plot.ScaleOptions["scheme"];
11 | _dispatch: React.Dispatch;
12 | };
13 |
14 | export type ChartAction =
15 | | {
16 | type:
17 | | "SET_X"
18 | | "SET_Y"
19 | | "SET_COLOR"
20 | | "SET_MARK"
21 | | "SET_DATA"
22 | | "SET_COLUMNS";
23 | payload: Partial;
24 | }
25 | | {
26 | type: "RESET";
27 | }
28 | | {
29 | type: "INITIALIZE";
30 | payload: {
31 | data: { rows: unknown[]; columns: ResultColumn[] };
32 | options: Plot.AutoOptions;
33 | };
34 | }
35 | | {
36 | type: "SET_SCHEME";
37 | payload: {
38 | scheme: Plot.ScaleOptions["scheme"];
39 | };
40 | };
41 |
--------------------------------------------------------------------------------
/apps/web/app/components/plot/context/useChart.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { ChartContext } from "./context";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 |
6 | /**
7 | * Hook to manage the state of the chart.
8 | */
9 | export function useChart() {
10 | const context = useContext(ChartContext);
11 | if (context === undefined) {
12 | throw new Error("useChart must be used within a ChartProvider");
13 | }
14 | return context;
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/app/components/plot/index.tsx:
--------------------------------------------------------------------------------
1 | import * as Plot from "@observablehq/plot";
2 | import { memo, useEffect, useRef } from "react";
3 | import { toast } from "sonner";
4 | import { cn } from "~/lib/utils";
5 | import { useChart } from "./context/useChart";
6 |
7 | type ChartProps = {
8 | containerClassName?: React.HTMLProps["className"];
9 | };
10 |
11 | /**
12 | * Still a work in progress...
13 | */
14 | const Chart = memo(function Chart(props: ChartProps) {
15 | const containerRef = useRef(null);
16 | const { data, scheme, x, y, color, mark } = useChart();
17 |
18 | useEffect(() => {
19 | let plot: (HTMLElement | SVGSVGElement) & Plot.Plot;
20 |
21 | if (!containerRef.current) return;
22 |
23 | if (!data || !x || !y) {
24 | return;
25 | }
26 |
27 | // #TODO: investigate and improve.
28 | try {
29 | const autoMark = Plot.auto(data, {
30 | x,
31 | y,
32 | color,
33 | mark,
34 | });
35 |
36 | plot = Plot.plot({
37 | color: { scheme },
38 | marks: [autoMark],
39 | marginBottom: 40,
40 | marginLeft: 100,
41 | marginRight: 40,
42 | marginTop: 80,
43 | });
44 |
45 | containerRef.current.append(plot);
46 | } catch (e) {
47 | console.error("Error creating plot: ", e);
48 | toast.error("Error creating plot", {
49 | description: e instanceof Error ? e.message : "Unknown error",
50 | });
51 | }
52 |
53 | return () => {
54 | if (plot) {
55 | plot.remove();
56 | }
57 | };
58 | }, [x, y, scheme, data, color, mark]);
59 |
60 | return (
61 |
69 | );
70 | });
71 |
72 | export default Chart;
73 |
--------------------------------------------------------------------------------
/apps/web/app/components/table/help.ts:
--------------------------------------------------------------------------------
1 | // // https://github.com/TobikoData/sqlmesh/blob/main/web/client/src/library/components/table/help.ts
2 | // import { type Table } from "apache-arrow";
3 | // import { isNil } from "~/utils";
4 |
5 | // type TableCellValue = number | string | null;
6 | // export type TableRow = Record;
7 | // export interface TableColumn {
8 | // name: string;
9 | // type: string;
10 | // }
11 | // type ResponseTableColumns = Array>;
12 |
13 | // export function getTableDataFromArrowStreamResult(
14 | // result: Table,
15 | // ): [TableColumn[], TableRow[]] {
16 | // if (isNil(result)) return [[], []];
17 |
18 | // const data: ResponseTableColumns = result.toArray(); // result.toArray() returns an array of Proxies
19 | // const rows = Array.from(data).map(toTableRow); // using Array.from to convert the Proxies to real objects
20 | // const columns = result.schema.fields.map((field) => ({
21 | // name: field.name,
22 | // type: getColumnType(field.type.toString()),
23 | // }));
24 |
25 | // return [columns, rows];
26 | // }
27 |
28 | // function toTableRow(
29 | // row: Array<[string, TableCellValue]> = [],
30 | // ): Record {
31 | // // using Array.from to convert the Proxies to real objects
32 | // return Array.from(row).reduce(
33 | // (acc, [key, value]) =>
34 | // Object.assign(acc, { [key]: isNil(value) ? undefined : String(value) }),
35 | // {},
36 | // );
37 | // }
38 |
39 | // function getColumnType(type: string): string {
40 | // if (type === "Int32" || type === "Int64") return "int";
41 | // if (type === "Float64") return "float";
42 |
43 | // return "text";
44 | // }
45 |
--------------------------------------------------------------------------------
/apps/web/app/components/tag.tsx:
--------------------------------------------------------------------------------
1 | // Source: https://github.com/latentcat/uvcanvas/blob/main/site/src/components/Tag.tsx
2 | import clsx from "clsx";
3 |
4 | const variantStyles = {
5 | small: "",
6 | medium: "rounded-lg px-1.5 ring-1 ring-inset",
7 | };
8 |
9 | const colorStyles = {
10 | emerald: {
11 | small: "text-emerald-500 dark:text-emerald-400",
12 | medium:
13 | "ring-emerald-300 dark:ring-emerald-400/30 bg-emerald-400/10 text-emerald-500 dark:text-emerald-400",
14 | },
15 | sky: {
16 | small: "text-sky-500",
17 | medium:
18 | "ring-sky-300 bg-sky-400/10 text-sky-500 dark:ring-sky-400/30 dark:bg-sky-400/10 dark:text-sky-400",
19 | },
20 | amber: {
21 | small: "text-amber-500",
22 | medium:
23 | "ring-amber-300 bg-amber-400/10 text-amber-500 dark:ring-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400",
24 | },
25 | rose: {
26 | small: "text-red-500 dark:text-rose-500",
27 | medium:
28 | "ring-rose-200 bg-rose-50 text-red-500 dark:ring-rose-500/20 dark:bg-rose-400/10 dark:text-rose-400",
29 | },
30 | zinc: {
31 | small: "text-zinc-400 dark:text-zinc-500",
32 | medium:
33 | "ring-zinc-200 bg-zinc-50 text-zinc-500 dark:ring-zinc-500/20 dark:bg-zinc-400/10 dark:text-zinc-400",
34 | },
35 | };
36 |
37 | const valueColorMap = {
38 | GET: "emerald",
39 | POST: "sky",
40 | PUT: "amber",
41 | DELETE: "rose",
42 | } as Record;
43 |
44 | export function Tag({
45 | children,
46 | variant = "medium",
47 | color = valueColorMap[children] ?? "emerald",
48 | }: {
49 | children: keyof typeof valueColorMap & (string | object);
50 | variant?: keyof typeof variantStyles;
51 | color?: keyof typeof colorStyles;
52 | }) {
53 | return (
54 |
61 | {children}
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/apps/web/app/components/tailwind-indicator.tsx:
--------------------------------------------------------------------------------
1 | export function TailwindIndicator() {
2 | // don't show in production
3 | if (import.meta.env.PROD) return null;
4 |
5 | return (
6 |
7 |
xs
8 |
sm
9 |
md
10 |
lg
11 |
xl
12 |
2xl
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/app/components/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { Moon, Sun } from "lucide-react";
2 | import { Theme, useTheme } from "remix-themes";
3 |
4 | import { AnimatePresence, motion } from "framer-motion";
5 | import { Button } from "./ui/button";
6 |
7 | const MotionSun = motion(Sun);
8 | const MotionMoon = motion(Moon);
9 | const MotionButton = motion(Button);
10 |
11 | export default function ModeToggle() {
12 | const [theme, setTheme] = useTheme();
13 |
14 | const isDark = theme === Theme.DARK;
15 |
16 | return (
17 |
22 | setTheme((s) => (s === Theme.DARK ? Theme.LIGHT : Theme.DARK))
23 | }
24 | >
25 |
26 | {isDark ? (
27 |
42 | ) : (
43 |
58 | )}
59 |
60 |
61 | Toggle theme
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
2 | import { ChevronDownIcon } from "@radix-ui/react-icons";
3 | import * as React from "react";
4 | import { cn } from "~/lib/utils";
5 |
6 | const Accordion = AccordionPrimitive.Root;
7 |
8 | const AccordionItem = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
17 | ));
18 | AccordionItem.displayName = "AccordionItem";
19 |
20 | const AccordionTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, children, ...props }, ref) => (
24 |
25 | svg]:rotate-180",
29 | className,
30 | )}
31 | {...props}
32 | >
33 | {children}
34 |
35 |
36 |
37 | ));
38 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
39 |
40 | const AccordionContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, children, ...props }, ref) => (
44 |
49 | {children}
50 |
51 | ));
52 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
53 |
54 | export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
55 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import { cva, type VariantProps } from "class-variance-authority";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const alertVariants = cva(
6 | "[&>svg]:text-foreground relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7",
7 | {
8 | variants: {
9 | variant: {
10 | default: "bg-background text-foreground",
11 | destructive:
12 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
13 | },
14 | },
15 | defaultVariants: {
16 | variant: "default",
17 | },
18 | },
19 | );
20 |
21 | const Alert = React.forwardRef<
22 | HTMLDivElement,
23 | React.HTMLAttributes & VariantProps
24 | >(({ className, variant, ...props }, ref) => (
25 |
31 | ));
32 | Alert.displayName = "Alert";
33 |
34 | const AlertTitle = React.forwardRef<
35 | HTMLParagraphElement,
36 | React.HTMLAttributes
37 | >(({ className, ...props }, ref) => (
38 |
43 | ));
44 | AlertTitle.displayName = "AlertTitle";
45 |
46 | const AlertDescription = React.forwardRef<
47 | HTMLParagraphElement,
48 | React.HTMLAttributes
49 | >(({ className, ...props }, ref) => (
50 |
55 | ));
56 | AlertDescription.displayName = "AlertDescription";
57 |
58 | export { Alert, AlertDescription, AlertTitle };
59 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2 |
3 | const AspectRatio = AspectRatioPrimitive.Root
4 |
5 | export { AspectRatio }
6 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const Avatar = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Avatar.displayName = AvatarPrimitive.Root.displayName;
19 |
20 | const AvatarImage = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
31 |
32 | const AvatarFallback = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
46 |
47 | export { Avatar, AvatarFallback, AvatarImage };
48 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/badge/badge.tsx:
--------------------------------------------------------------------------------
1 | import { type VariantProps } from "class-variance-authority";
2 | import type { HTMLAttributes } from "react";
3 | import { forwardRef } from "react";
4 | import { cn } from "~/lib/utils";
5 | import { badgeVariants } from "./variants"; // NB. Don't reference index.js or rollup complains.
6 |
7 | export interface BadgeProps
8 | extends HTMLAttributes,
9 | VariantProps {}
10 |
11 | const Badge = forwardRef(
12 | ({ className, variant, ...props }, ref) => {
13 | return (
14 |
19 | );
20 | },
21 | );
22 |
23 | export { Badge };
24 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/badge/index.ts:
--------------------------------------------------------------------------------
1 | export { Badge } from "./badge";
2 | export { badgeVariants } from "./variants";
3 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/badge/variants.tsx:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const badgeVariants = cva(
4 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
5 | {
6 | variants: {
7 | variant: {
8 | default:
9 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
10 | secondary:
11 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
12 | destructive:
13 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
14 | outline: "text-foreground",
15 | },
16 | },
17 | defaultVariants: {
18 | variant: "default",
19 | },
20 | },
21 | );
22 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/button/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot";
2 | import { type VariantProps } from "class-variance-authority";
3 | import * as React from "react";
4 | import { cn } from "~/lib/utils";
5 | import { buttonVariants } from "./variants";
6 |
7 | export interface ButtonProps
8 | extends React.ButtonHTMLAttributes,
9 | VariantProps {
10 | asChild?: boolean;
11 | }
12 |
13 | const Button = React.forwardRef(
14 | ({ className, variant, size, asChild = false, ...props }, ref) => {
15 | const Comp = asChild ? Slot : "button";
16 | return (
17 |
22 | );
23 | },
24 | );
25 | Button.displayName = "Button";
26 |
27 | export { Button };
28 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/button/index.ts:
--------------------------------------------------------------------------------
1 | export { Button } from "./button";
2 | export { buttonVariants } from "./variants";
3 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/button/variants.tsx:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const buttonVariants = cva(
4 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
5 | {
6 | variants: {
7 | variant: {
8 | default:
9 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
10 | destructive:
11 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
12 | outline:
13 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
14 | secondary:
15 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
16 | ghost: "hover:bg-accent hover:text-accent-foreground",
17 | link: "text-primary underline-offset-4 hover:underline",
18 | success: "bg-[#30a46c] text-white shadow-sm hover:bg-[#2b9a66]",
19 | },
20 | size: {
21 | default: "h-9 px-4 py-2",
22 | sm: "h-8 rounded-md px-3 text-xs",
23 | lg: "h-10 rounded-md px-8",
24 | icon: "size-9",
25 | xs: "h-6 rounded-md px-2 text-xs",
26 | },
27 | },
28 | defaultVariants: {
29 | variant: "default",
30 | size: "default",
31 | },
32 | },
33 | );
34 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";
4 | import type * as React from "react";
5 | import { DayPicker } from "react-day-picker";
6 | import { cn } from "~/lib/utils";
7 | import { buttonVariants } from "./button";
8 |
9 | export type CalendarProps = React.ComponentProps;
10 |
11 | function Calendar({
12 | className,
13 | classNames,
14 | showOutsideDays = true,
15 | ...props
16 | }: CalendarProps) {
17 | return (
18 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
42 | : "[&:has([aria-selected])]:rounded-md",
43 | ),
44 | day: cn(
45 | buttonVariants({ variant: "ghost" }),
46 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100",
47 | ),
48 | day_range_start: "day-range-start",
49 | day_range_end: "day-range-end",
50 | day_selected:
51 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
52 | day_today: "bg-accent text-accent-foreground",
53 | day_outside:
54 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
55 | day_disabled: "text-muted-foreground opacity-50",
56 | day_range_middle:
57 | "aria-selected:bg-accent aria-selected:text-accent-foreground",
58 | day_hidden: "invisible",
59 | ...classNames,
60 | }}
61 | components={{
62 | IconLeft: () => ,
63 | IconRight: () => ,
64 | }}
65 | {...props}
66 | />
67 | );
68 | }
69 | Calendar.displayName = "Calendar";
70 |
71 | export { Calendar };
72 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cn } from "~/lib/utils";
3 |
4 | const Card = React.forwardRef<
5 | HTMLDivElement,
6 | React.HTMLAttributes
7 | >(({ className, ...props }, ref) => (
8 |
16 | ));
17 | Card.displayName = "Card";
18 |
19 | const CardHeader = React.forwardRef<
20 | HTMLDivElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
28 | ));
29 | CardHeader.displayName = "CardHeader";
30 |
31 | const CardTitle = React.forwardRef<
32 | HTMLParagraphElement,
33 | React.HTMLAttributes
34 | >(({ className, ...props }, ref) => (
35 | // eslint-disable-next-line jsx-a11y/heading-has-content
36 |
41 | ));
42 | CardTitle.displayName = "CardTitle";
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ));
54 | CardDescription.displayName = "CardDescription";
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
65 | ));
66 | CardContent.displayName = "CardContent";
67 |
68 | const CardFooter = React.forwardRef<
69 | HTMLDivElement,
70 | React.HTMLAttributes
71 | >(({ className, ...props }, ref) => (
72 |
77 | ));
78 | CardFooter.displayName = "CardFooter";
79 |
80 | export {
81 | Card,
82 | CardContent,
83 | CardDescription,
84 | CardFooter,
85 | CardHeader,
86 | CardTitle,
87 | };
88 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
2 | import { CheckIcon } from "@radix-ui/react-icons";
3 | import * as React from "react";
4 | import { cn } from "~/lib/utils";
5 |
6 | const Checkbox = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
21 |
22 |
23 |
24 | ));
25 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
26 |
27 | export { Checkbox };
28 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
2 |
3 | const Collapsible = CollapsiblePrimitive.Root;
4 |
5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
6 |
7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
8 |
9 | export { Collapsible, CollapsibleContent, CollapsibleTrigger };
10 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/combobox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
4 | import * as React from "react";
5 | import { Button } from "~/components/ui/button/button";
6 | import {
7 | Command,
8 | CommandEmpty,
9 | CommandGroup,
10 | CommandInput,
11 | CommandItem,
12 | } from "~/components/ui/command";
13 | import {
14 | Popover,
15 | PopoverContent,
16 | PopoverTrigger,
17 | } from "~/components/ui/popover";
18 | import { cn } from "~/lib/utils";
19 |
20 | const frameworks = [
21 | {
22 | value: "next.js",
23 | label: "Next.js",
24 | },
25 | {
26 | value: "sveltekit",
27 | label: "SvelteKit",
28 | },
29 | {
30 | value: "nuxt.js",
31 | label: "Nuxt.js",
32 | },
33 | {
34 | value: "remix",
35 | label: "Remix",
36 | },
37 | {
38 | value: "astro",
39 | label: "Astro",
40 | },
41 | ];
42 |
43 | export function ComboboxDemo() {
44 | const [open, setOpen] = React.useState(false);
45 | const [value, setValue] = React.useState("");
46 |
47 | return (
48 |
52 |
53 |
64 |
65 |
66 |
67 |
71 | No framework found.
72 |
73 | {frameworks.map((framework) => (
74 | {
78 | setValue(currentValue === value ? "" : currentValue);
79 | setOpen(false);
80 | }}
81 | >
82 | {framework.label}
83 |
89 |
90 | ))}
91 |
92 |
93 |
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/form/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import type { FieldPath, FieldValues } from "react-hook-form";
3 |
4 | type FormFieldContextValue<
5 | TFieldValues extends FieldValues = FieldValues,
6 | TName extends FieldPath = FieldPath,
7 | > = {
8 | name: TName;
9 | };
10 |
11 | export const FormFieldContext = createContext(
12 | {} as FormFieldContextValue,
13 | );
14 |
15 | type FormItemContextValue = {
16 | id: string;
17 | };
18 |
19 | export const FormItemContext = createContext(
20 | {} as FormItemContextValue,
21 | );
22 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/form/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | Form,
3 | FormControl,
4 | FormDescription,
5 | FormField,
6 | FormItem,
7 | FormLabel,
8 | FormMessage,
9 | } from "./form";
10 | export { useFormField } from "./useFormField";
11 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/form/useFormField.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { useFormContext } from "react-hook-form";
3 | import { FormFieldContext, FormItemContext } from "./context";
4 |
5 | export const useFormField = () => {
6 | const fieldContext = useContext(FormFieldContext);
7 | const itemContext = useContext(FormItemContext);
8 | const { getFieldState, formState } = useFormContext();
9 |
10 | const fieldState = getFieldState(fieldContext.name, formState);
11 |
12 | if (!fieldContext) {
13 | throw new Error("useFormField should be used within ");
14 | }
15 |
16 | const { id } = itemContext;
17 |
18 | return {
19 | id,
20 | name: fieldContext.name,
21 | formItemId: `${id}-form-item`,
22 | formDescriptionId: `${id}-form-item-description`,
23 | formMessageId: `${id}-form-item-message`,
24 | ...fieldState,
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const HoverCard = HoverCardPrimitive.Root;
6 |
7 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
8 |
9 | const HoverCardContent = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
13 |
23 | ));
24 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
25 |
26 | export { HoverCard, HoverCardContent, HoverCardTrigger };
27 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cn } from "~/lib/utils";
3 |
4 | export interface InputProps
5 | extends React.InputHTMLAttributes {}
6 |
7 | const Input = React.forwardRef(
8 | ({ className, type, ...props }, ref) => {
9 | return (
10 |
19 | );
20 | },
21 | );
22 | Input.displayName = "Input";
23 |
24 | export { Input };
25 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as LabelPrimitive from "@radix-ui/react-label";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 | import * as React from "react";
4 | import { cn } from "~/lib/utils";
5 |
6 | const labelVariants = cva(
7 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
8 | );
9 |
10 | const Label = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef &
13 | VariantProps
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | Label.displayName = LabelPrimitive.Root.displayName;
22 |
23 | export { Label };
24 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/link/index.ts:
--------------------------------------------------------------------------------
1 | import { buttonVariants } from "../button/variants";
2 |
3 | export { StyledLink } from "./link";
4 | export { buttonVariants as linkVariants };
5 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/link/link.tsx:
--------------------------------------------------------------------------------
1 | import { Link, type LinkProps as RemixLinkProps } from "@remix-run/react";
2 | import { type VariantProps } from "class-variance-authority";
3 | import * as React from "react";
4 | import { cn } from "~/lib/utils";
5 | import { buttonVariants } from "../button";
6 |
7 | export type LinkProps = RemixLinkProps & VariantProps;
8 |
9 | const StyledLink = React.forwardRef(
10 | ({ className, variant, size, ...props }, ref) => {
11 | return (
12 |
17 | );
18 | },
19 | );
20 | StyledLink.displayName = "Link";
21 |
22 | export { StyledLink };
23 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/navigation-menu/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | NavigationMenu,
3 | NavigationMenuContent,
4 | NavigationMenuIndicator,
5 | NavigationMenuItem,
6 | NavigationMenuLink,
7 | NavigationMenuList,
8 | NavigationMenuTrigger,
9 | NavigationMenuViewport,
10 | } from "./navigation-menu";
11 | export { navigationMenuTriggerStyle } from "./trigger-style";
12 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/navigation-menu/trigger-style.tsx:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const navigationMenuTriggerStyle = cva(
4 | "bg-background hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[active]:bg-accent/50 data-[state=open]:bg-accent/50 group inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors focus:outline-none disabled:pointer-events-none disabled:opacity-50",
5 | );
6 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/pill/index.ts:
--------------------------------------------------------------------------------
1 | export { Pill } from "./pill";
2 | export { pillVariants } from "./variants";
3 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/pill/pill.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot";
2 | import { type VariantProps } from "class-variance-authority";
3 | import * as React from "react";
4 | import { cn } from "~/lib/utils";
5 | import { pillVariants } from "./variants";
6 |
7 | export interface PillProps
8 | extends React.HTMLAttributes,
9 | VariantProps {
10 | asChild?: boolean;
11 | }
12 |
13 | /**
14 | * Custom pill component.
15 | *
16 | * Styling based on [Pines](https://devdojo.com/pines/docs/badge).
17 | */
18 | const Pill = React.forwardRef(
19 | ({ className, variant, size, asChild = false, ...props }, ref) => {
20 | const Comp = asChild ? Slot : "span";
21 | return (
22 |
27 | );
28 | },
29 | );
30 | Pill.displayName = "Pill";
31 |
32 | export { Pill };
33 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/pill/variants.tsx:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const pillVariants = cva(
4 | "inline-flex items-center rounded-full text-xs font-medium",
5 | {
6 | variants: {
7 | variant: {
8 | default:
9 | "bg-secondary text-primary-foreground shadow hover:bg-primary/90",
10 | },
11 | size: {
12 | default: "px-1.5 py-0.5",
13 | },
14 | },
15 | defaultVariants: {
16 | variant: "default",
17 | size: "default",
18 | },
19 | },
20 | );
21 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as PopoverPrimitive from "@radix-ui/react-popover";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const Popover = PopoverPrimitive.Root;
6 |
7 | const PopoverTrigger = PopoverPrimitive.Trigger;
8 |
9 | const PopoverContent = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
13 |
14 |
24 |
25 | ));
26 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
27 |
28 | export { Popover, PopoverContent, PopoverTrigger };
29 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as ProgressPrimitive from "@radix-ui/react-progress";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const Progress = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, value, ...props }, ref) => (
9 |
17 |
21 |
22 | ));
23 | Progress.displayName = ProgressPrimitive.Root.displayName;
24 |
25 | export { Progress };
26 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import { CheckIcon } from "@radix-ui/react-icons";
2 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
3 | import * as React from "react";
4 | import { cn } from "~/lib/utils";
5 |
6 | const RadioGroup = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => {
10 | return (
11 |
16 | );
17 | });
18 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
19 |
20 | const RadioGroupItem = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => {
24 | return (
25 |
33 |
34 |
35 |
36 |
37 | );
38 | });
39 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
40 |
41 | export { RadioGroup, RadioGroupItem };
42 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/resizeable.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { DragHandleDots2Icon } from "@radix-ui/react-icons";
4 | import * as ResizablePrimitive from "react-resizable-panels";
5 | import { cn } from "~/lib/utils";
6 |
7 | function ResizablePanelGroup({
8 | className,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
19 | );
20 | }
21 |
22 | const ResizablePanel = ResizablePrimitive.Panel;
23 |
24 | function ResizableHandle({
25 | withHandle,
26 | className,
27 | ...props
28 | }: React.ComponentProps & {
29 | withHandle?: boolean;
30 | }) {
31 | return (
32 | div]:rotate-90",
35 | className,
36 | )}
37 | {...props}
38 | >
39 | {withHandle && (
40 |
41 |
42 |
43 | )}
44 |
45 | );
46 | }
47 |
48 | export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
49 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const ScrollArea = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, children, ...props }, ref) => (
9 |
14 |
15 | {children}
16 |
17 |
18 |
19 |
20 | ));
21 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
22 |
23 | const ScrollBar = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, orientation = "vertical", ...props }, ref) => (
27 |
40 |
41 |
42 | ));
43 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
44 |
45 | export { ScrollArea, ScrollBar };
46 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const Separator = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(
9 | (
10 | { className, orientation = "horizontal", decorative = true, ...props },
11 | ref,
12 | ) => (
13 |
24 | ),
25 | );
26 | Separator.displayName = SeparatorPrimitive.Root.displayName;
27 |
28 | export { Separator };
29 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "~/lib/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as SliderPrimitive from "@radix-ui/react-slider";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const Slider = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 |
18 |
19 |
20 |
21 |
22 | ));
23 | Slider.displayName = SliderPrimitive.Root.displayName;
24 |
25 | export { Slider };
26 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "remix-themes";
2 | import { Toaster as Sonner } from "sonner";
3 |
4 | type ToasterProps = React.ComponentProps;
5 |
6 | function Toaster({ ...props }: ToasterProps) {
7 | const [theme] = useTheme();
8 |
9 | return (
10 |
26 | );
27 | }
28 |
29 | export { Toaster };
30 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as SwitchPrimitives from "@radix-ui/react-switch";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const Switch = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 |
22 |
23 | ));
24 | Switch.displayName = SwitchPrimitives.Root.displayName;
25 |
26 | export { Switch };
27 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as TabsPrimitive from "@radix-ui/react-tabs";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const Tabs = TabsPrimitive.Root;
6 |
7 | const TabsList = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 | ));
20 | TabsList.displayName = TabsPrimitive.List.displayName;
21 |
22 | const TabsTrigger = React.forwardRef<
23 | React.ElementRef,
24 | React.ComponentPropsWithoutRef
25 | >(({ className, ...props }, ref) => (
26 |
34 | ));
35 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
36 |
37 | const TabsContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, ...props }, ref) => (
41 |
49 | ));
50 | TabsContent.displayName = TabsPrimitive.Content.displayName;
51 |
52 | export { Tabs, TabsContent, TabsList, TabsTrigger };
53 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cn } from "~/lib/utils";
3 |
4 | export interface TextareaProps
5 | extends React.TextareaHTMLAttributes {}
6 |
7 | const Textarea = React.forwardRef(
8 | ({ className, ...props }, ref) => {
9 | return (
10 |
18 | );
19 | },
20 | );
21 | Textarea.displayName = "Textarea";
22 |
23 | export { Textarea };
24 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Toast,
3 | ToastClose,
4 | ToastDescription,
5 | ToastProvider,
6 | ToastTitle,
7 | ToastViewport,
8 | } from "~/components/ui/toast";
9 | import { useToast } from "~/components/ui/use-toast";
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast();
13 |
14 | return (
15 |
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 |
22 |
23 | {title && {title}}
24 | {description && (
25 | {description}
26 | )}
27 |
28 | {action}
29 |
30 |
31 | );
32 | })}
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
2 | import { type VariantProps } from "class-variance-authority";
3 | import * as React from "react";
4 | import { toggleVariants } from "~/components/ui/toggle";
5 | import { cn } from "~/lib/utils";
6 |
7 | const ToggleGroupContext = React.createContext<
8 | VariantProps
9 | >({
10 | size: "default",
11 | variant: "default",
12 | });
13 |
14 | const ToggleGroup = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef &
17 | VariantProps
18 | >(({ className, variant, size, children, ...props }, ref) => (
19 |
24 |
25 | {children}
26 |
27 |
28 | ));
29 |
30 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
31 |
32 | const ToggleGroupItem = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef &
35 | VariantProps
36 | >(({ className, children, variant, size, ...props }, ref) => {
37 | const context = React.useContext(ToggleGroupContext);
38 |
39 | return (
40 |
51 | {children}
52 |
53 | );
54 | });
55 |
56 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
57 |
58 | export { ToggleGroup, ToggleGroupItem };
59 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/toggle/index.ts:
--------------------------------------------------------------------------------
1 | export { Toggle } from "./toggle";
2 | export { toggleVariants } from "./variants";
3 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/toggle/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as TogglePrimitive from "@radix-ui/react-toggle";
2 | import { type VariantProps } from "class-variance-authority";
3 | import * as React from "react";
4 | import { cn } from "~/lib/utils";
5 | import { toggleVariants } from ".";
6 |
7 | const Toggle = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef &
10 | VariantProps
11 | >(({ className, variant, size, ...props }, ref) => (
12 |
17 | ));
18 |
19 | Toggle.displayName = TogglePrimitive.Root.displayName;
20 |
21 | export { Toggle };
22 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/toggle/variants.tsx:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const toggleVariants = cva(
4 | "hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
5 | {
6 | variants: {
7 | variant: {
8 | default: "bg-transparent",
9 | outline:
10 | "border-input hover:bg-accent hover:text-accent-foreground border bg-transparent shadow-sm",
11 | },
12 | size: {
13 | default: "h-9 px-3",
14 | sm: "h-8 px-2",
15 | lg: "h-10 px-3",
16 | },
17 | },
18 | defaultVariants: {
19 | variant: "default",
20 | size: "default",
21 | },
22 | },
23 | );
24 |
--------------------------------------------------------------------------------
/apps/web/app/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2 | import * as React from "react";
3 | import { cn } from "~/lib/utils";
4 |
5 | const TooltipProvider = TooltipPrimitive.Provider;
6 |
7 | const Tooltip = TooltipPrimitive.Root;
8 |
9 | const TooltipTrigger = TooltipPrimitive.Trigger;
10 |
11 | const TooltipContent = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, sideOffset = 4, ...props }, ref) => (
15 |
16 |
25 |
26 | ));
27 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
28 |
29 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
30 |
--------------------------------------------------------------------------------
/apps/web/app/constants.client.ts:
--------------------------------------------------------------------------------
1 | // ----------- App ------------ //
2 |
3 | export const prodDomain = "www.quackdb.com";
4 |
5 | // ----------- Query ------------ //
6 |
7 | /**
8 | * IndexedDB cache (accessed through idb-keyval).
9 | */
10 | export const IDB_KEYS = {
11 | QUERY_HISTORY: "query-history", // the actual SQL query runs
12 | };
13 |
14 | /**
15 | * Caches API keys.
16 | */
17 | export const CACHE_KEYS = {
18 | QUERY_RESULTS: "query-result", // the result of the SQL query for caching in caches.
19 | };
20 |
21 | export const LOCAL_STORAGE_KEYS = {};
22 |
23 | export const sidebarWidth = 20;
24 |
--------------------------------------------------------------------------------
/apps/web/app/constants.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Meta information for the app.
3 | */
4 | export const metaDetails = {
5 | themeColor: "#0a0a0a",
6 | description:
7 | "QuackDB: Explore the full power of DuckDB with our open-source, in-browser SQL editor. Designed for quick prototyping, data tasks, and visualizations, all while preserving your privacy.",
8 | msapplicationTileColor: "#00aba9",
9 | };
10 |
--------------------------------------------------------------------------------
/apps/web/app/context/db/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import type { DBState } from "./types";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 | export const DBContext = createContext(undefined);
6 |
--------------------------------------------------------------------------------
/apps/web/app/context/db/provider.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useRef } from "react";
2 | import { useSession } from "~/context/session/useSession";
3 | import { DuckDBInstance } from "~/modules/duckdb-singleton";
4 | import { type Dataset } from "~/types/files/dataset";
5 | import { DBContext } from "./context";
6 |
7 | type DBProviderProps = { children: React.ReactNode };
8 |
9 | // Breakup everything into smaller files because of React Fast Refresh limitations.
10 |
11 | /**
12 | * Context for the DuckDB instance.
13 | *
14 | * We want to create a single instance of DuckDB and share it across the app.
15 | * Rather create as many connections to the DB as needed.
16 | */
17 | function DbProvider(props: DBProviderProps) {
18 | // React docs recommend avoiding creating a new instance on every render (if expensive).
19 | // So rather use the if(db.current === null) pattern.
20 | // See: https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents
21 |
22 | /**
23 | * https://react.dev/learn/referencing-values-with-refs#best-practices-for-refs
24 | *
25 | * Don’t read or write ref.current during rendering. If some information is needed during rendering, use state instead.
26 | * Since React doesn’t know when ref.current changes, even reading it while rendering makes your component’s behavior difficult to predict.
27 | * (The only exception to this is code like if (!ref.current) ref.current = new Thing() which only sets the ref once during the first render.)
28 | *
29 | * i.e. the dbRef isn't used in the render (not displayed visually or used to calculate the next state), so it's safe to use a ref.
30 | */
31 |
32 | const db = useRef(null);
33 | const { sessionId, sources } = useSession();
34 |
35 | if (db.current === null) {
36 | // don't need to wait for first render to create the instance (and avoid creating a new instance on every render)
37 | db.current = new DuckDBInstance({
38 | session: sessionId,
39 | });
40 | }
41 |
42 | // cleanup on unmount (not session change)
43 | useEffect(() => {
44 | return () => {
45 | db.current
46 | ?.dispose()
47 | .catch((e) => console.error("Error disposing db: ", e));
48 | };
49 | }, []);
50 |
51 | // add sources to db
52 | useEffect(() => {
53 | const abortController = new AbortController();
54 | const signal = abortController.signal;
55 |
56 | const addSources = async (sources: Dataset[]) => {
57 | try {
58 | for await (const source of sources) {
59 | if (signal.aborted) break;
60 | await db.current?.registerFileHandle(source.path, source.handle);
61 | }
62 | } catch (e) {
63 | if (e instanceof DOMException && e.name === "AbortError") return;
64 | console.error("Error adding sources: ", e);
65 | }
66 | };
67 |
68 | addSources(sources);
69 |
70 | return () => {
71 | abortController.abort();
72 | };
73 | }, [sources]);
74 |
75 | const value = useMemo(
76 | () => ({
77 | db: db.current,
78 | }),
79 | [],
80 | );
81 | return (
82 | {props.children}
83 | );
84 | }
85 |
86 | export { DbProvider };
87 |
--------------------------------------------------------------------------------
/apps/web/app/context/db/types.ts:
--------------------------------------------------------------------------------
1 | import type { DuckDBInstance } from "~/modules/duckdb-singleton";
2 |
3 | export type DBState = {
4 | db: DuckDBInstance | null;
5 | };
6 |
--------------------------------------------------------------------------------
/apps/web/app/context/db/useDB.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { DBContext } from "./context";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 |
6 | /**
7 | * Hook to access the DuckDB instance
8 | */
9 | export function useDB() {
10 | const context = useContext(DBContext);
11 | if (context === undefined) {
12 | throw new Error("useDB must be used within a DBContext");
13 | }
14 | return context;
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/app/context/editor-settings/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import type { EditorSettingsContextValue } from "./types";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 | export const EditorSettingsContext = createContext<
6 | EditorSettingsContextValue | undefined
7 | >(undefined);
8 |
--------------------------------------------------------------------------------
/apps/web/app/context/editor-settings/provider.tsx:
--------------------------------------------------------------------------------
1 | import { useLocalStorage } from "@uidotdev/usehooks";
2 | import { useCallback, useMemo } from "react";
3 | import { EditorSettingsContext } from "./context";
4 |
5 | type EditorSettingsProviderProps = { children: React.ReactNode };
6 |
7 | // Breakup everything into smaller files because of React Fast Refresh limitations.
8 |
9 | /**
10 | * Context provider for monaco editor instance.
11 | *
12 | * Client side only.
13 | */
14 | function EditorSettingsProvider(props: EditorSettingsProviderProps) {
15 | const [shouldFormat, setShouldFormat] = useLocalStorage("shouldFormat", true);
16 |
17 | /**
18 | * Potential settings to add in the future.
19 | */
20 | // const [darkTheme, setDarkTheme] = useLocalStorage("darkTheme", true);
21 | // const [lightTheme, setLightTheme] = useLocalStorage("lightTheme", false);
22 | // const [theme, setTheme] = useLocalStorage("theme", "dark");
23 | // const [fontSize, setFontSize] = useLocalStorage("fontSize", 14);
24 | // const [tabSize, setTabSize] = useLocalStorage("tabSize", 2);
25 | // const [wordWrap, setWordWrap] = useLocalStorage("wordWrap", "on");
26 | // const [lineNumbers, setLineNumbers] = useLocalStorage("lineNumbers", "on");
27 | // const [fontFamily, setFontFamily] = useLocalStorage("fontFamily", "Fira Code");
28 | // const [lineHeight, setLineHeight] = useLocalStorage("lineHeight", 1.5);
29 | // const [formatOnSave, setFormatOnSave] = useLocalStorage("formatOnSave", true);
30 | // const [formatOnPaste, setFormatOnPaste] = useLocalStorage("formatOnPaste", true);
31 | // const [formatOnType, setFormatOnType] = useLocalStorage("formatOnType", true);
32 |
33 | const toggleShouldFormat = useCallback(
34 | (shouldFormat: boolean) => setShouldFormat(shouldFormat),
35 | [setShouldFormat],
36 | );
37 |
38 | const value = useMemo(
39 | () => ({
40 | shouldFormat,
41 | toggleShouldFormat,
42 | }),
43 | [shouldFormat, toggleShouldFormat],
44 | );
45 | return (
46 |
47 | {props.children}
48 |
49 | );
50 | }
51 |
52 | export { EditorSettingsProvider };
53 |
--------------------------------------------------------------------------------
/apps/web/app/context/editor-settings/types.ts:
--------------------------------------------------------------------------------
1 | export type EditorSettingsState = {
2 | shouldFormat: boolean;
3 | };
4 |
5 | export type EditorSettingsContextValue = EditorSettingsState & {
6 | toggleShouldFormat: (shouldFormat: boolean) => void;
7 | };
8 |
--------------------------------------------------------------------------------
/apps/web/app/context/editor-settings/useEditor.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { EditorSettingsContext } from "./context";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 |
6 | /**
7 | * Hook to access editor settings.
8 | */
9 | export function useEditorSettings() {
10 | const context = useContext(EditorSettingsContext);
11 | if (context === undefined) {
12 | throw new Error(
13 | "useEditorSettings must be used within an EditorSettingsProvider",
14 | );
15 | }
16 | return context;
17 | }
18 |
--------------------------------------------------------------------------------
/apps/web/app/context/editor/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import type { EditorState } from "./types";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 | export const EditorContext = createContext(undefined);
6 |
--------------------------------------------------------------------------------
/apps/web/app/context/editor/provider.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useRef } from "react";
2 | import type { EditorForwardedRef } from "~/components/monaco";
3 | import { EditorContext } from "./context";
4 |
5 | type EditorProviderProps = { children: React.ReactNode };
6 |
7 | // Breakup everything into smaller files because of React Fast Refresh limitations.
8 |
9 | /**
10 | * Context provider for monaco editor instance.
11 | */
12 | function EditorProvider(props: EditorProviderProps) {
13 | const editorRef = useRef(null);
14 |
15 | // cleanup
16 | useEffect(() => {
17 | const editor = editorRef.current?.getEditor();
18 | return () => {
19 | editor?.dispose();
20 | };
21 | }, []);
22 |
23 | const value = useMemo(
24 | () => ({
25 | editorRef,
26 | }),
27 | [],
28 | );
29 | return (
30 |
31 | {props.children}
32 |
33 | );
34 | }
35 |
36 | export { EditorProvider };
37 |
--------------------------------------------------------------------------------
/apps/web/app/context/editor/types.ts:
--------------------------------------------------------------------------------
1 | import type { EditorForwardedRef } from "~/components/monaco";
2 |
3 | export type EditorState = {
4 | editorRef: React.MutableRefObject;
5 | };
6 |
--------------------------------------------------------------------------------
/apps/web/app/context/editor/useEditor.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { EditorContext } from "./context";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 |
6 | /**
7 | * Hook to access the query context.
8 | */
9 | export function useEditor() {
10 | const context = useContext(EditorContext);
11 | if (context === undefined) {
12 | throw new Error("useEditor must be used within an EditorProvider");
13 | }
14 | return context;
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/app/context/pagination/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import type { PaginationContextValue } from "./types";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 | export const PaginationContext = createContext<
6 | PaginationContextValue | undefined
7 | >(undefined);
8 |
--------------------------------------------------------------------------------
/apps/web/app/context/pagination/types.ts:
--------------------------------------------------------------------------------
1 | export type PaginationState = {
2 | count: number;
3 | limit: number;
4 | offset: number;
5 | canGoNext: boolean;
6 | canGoPrev: boolean;
7 | };
8 |
9 | export type PaginationContextValue = PaginationState & {
10 | goToFirstPage: () => void;
11 | goToLastPage: () => void;
12 | onNextPage: () => void;
13 | onPrevPage: () => void;
14 | goToPage: (page: number) => void;
15 | onLimitChange: (value: number) => void;
16 | onSetCount: (value: number) => void;
17 | };
18 |
19 | export type PaginationActions =
20 | | {
21 | type: "SET_LIMIT";
22 | payload: number;
23 | }
24 | | {
25 | type: "SET_OFFSET";
26 | payload: number;
27 | }
28 | | {
29 | type: "SET_COUNT";
30 | payload: number;
31 | }
32 | | {
33 | type: "GO_TO_FIRST_PAGE";
34 | }
35 | | {
36 | type: "GO_TO_LAST_PAGE";
37 | }
38 | | {
39 | type: "GO_TO_PAGE";
40 | payload: number;
41 | }
42 | | {
43 | type: "ON_NEXT_PAGE";
44 | }
45 | | {
46 | type: "ON_PREV_PAGE";
47 | };
48 |
--------------------------------------------------------------------------------
/apps/web/app/context/pagination/usePagination.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { PaginationContext } from "./context";
3 |
4 | /**
5 | * Hook to control the pagination state.
6 | */
7 | export function usePagination() {
8 | const context = useContext(PaginationContext);
9 |
10 | if (context === undefined) {
11 | throw new Error("usePagination must be used within a PaginationProvider");
12 | }
13 | return context;
14 | }
15 |
--------------------------------------------------------------------------------
/apps/web/app/context/panel/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import type { PanelState } from "./types";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 | export const PanelContext = createContext(undefined);
6 |
--------------------------------------------------------------------------------
/apps/web/app/context/panel/types.ts:
--------------------------------------------------------------------------------
1 | export type PanelFile = {
2 | name: string;
3 | handle: FileSystemFileHandle;
4 | };
5 |
6 | export type PanelState = {
7 | currentFile: PanelFile | null;
8 | files: PanelFile[];
9 | openFiles: PanelFile[];
10 | currentFileIndex: number;
11 | closeFile: (file: PanelFile) => void;
12 | openFile: (file: PanelFile) => void;
13 | };
14 |
--------------------------------------------------------------------------------
/apps/web/app/context/panel/usePanel.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { PanelContext } from "./context";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 |
6 | /**
7 | * Hook to get the session context.
8 | */
9 | export function usePanel() {
10 | const context = useContext(PanelContext);
11 | if (context === undefined) {
12 | throw new Error("usePanel must be used within a PanelProvider");
13 | }
14 | return context;
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/app/context/query/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import type { QueryContextValue } from "./types";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 | export const QueryContext = createContext(
6 | undefined,
7 | );
8 |
--------------------------------------------------------------------------------
/apps/web/app/context/query/types.ts:
--------------------------------------------------------------------------------
1 | import { type QueryResponse } from "~/types/query";
2 |
3 | /**
4 | * The state of the query context.
5 | *
6 | * Note: the status in the top level of the state is used to determine if the query is running or not.
7 | * The status in the meta is used to determine if the query was successful or not.
8 | */
9 | export type QueryState = QueryResponse & {
10 | status: "IDLE" | "RUNNING";
11 | sql: string;
12 | };
13 |
14 | export type QueryMethods = {
15 | onRunQuery: (sql: string) => Promise;
16 | onCancelQuery: (reason: string) => void;
17 | };
18 |
19 | export type QueryContextValue = QueryState & QueryMethods;
20 |
--------------------------------------------------------------------------------
/apps/web/app/context/query/useQuery.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { QueryContext } from "./context";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 |
6 | /**
7 | * Hook to access the query context.
8 | */
9 | export function useQuery() {
10 | const context = useContext(QueryContext);
11 | if (context === undefined) {
12 | throw new Error("useQuery must be used within a QueryProvider");
13 | }
14 | return context;
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/app/context/session/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import type { SessionContextValue } from "./types";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 | export const SessionContext = createContext(
6 | undefined,
7 | );
8 |
--------------------------------------------------------------------------------
/apps/web/app/context/session/data/newfile-content.ts:
--------------------------------------------------------------------------------
1 | export const newfileContents = `
2 | -- Query external JSON API and create a new table
3 | CREATE TABLE new_tbl AS SELECT * FROM read_json_auto('https://api.datamuse.com/words?ml=sql');
4 | SELECT * FROM new_tbl;
5 |
6 | -- Query a parquet file
7 | SELECT * FROM read_parquet('stores.parquet');
8 |
9 | -- Query a CSV file
10 | SELECT * FROM read_csv('stores.csv');
11 | `;
12 |
--------------------------------------------------------------------------------
/apps/web/app/context/session/useSession.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { SessionContext } from "./context";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 |
6 | /**
7 | * Hook to get the session context.
8 | *
9 | * Includes session information and accompanying file handles.
10 | */
11 | export function useSession() {
12 | const context = useContext(SessionContext);
13 | if (context === undefined) {
14 | throw new Error("useSession must be used within a SessionContext");
15 | }
16 | return context;
17 | }
18 |
--------------------------------------------------------------------------------
/apps/web/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * By default, Remix will handle hydrating your app on the client for you.
3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4 | * For more information, see https://remix.run/file-conventions/entry.client
5 | */
6 |
7 | import { RemixBrowser } from "@remix-run/react";
8 | import { StrictMode, startTransition } from "react";
9 | import { hydrateRoot } from "react-dom/client";
10 |
11 | startTransition(() => {
12 | hydrateRoot(
13 | document,
14 |
15 |
16 | ,
17 | );
18 | });
19 |
--------------------------------------------------------------------------------
/apps/web/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | import { RemixServer } from "@remix-run/react";
2 | import {
3 | handleRequest as vercelHandleRequest,
4 | type ActionFunctionArgs,
5 | type AppLoadContext,
6 | type EntryContext,
7 | type LoaderFunctionArgs,
8 | } from "@vercel/remix";
9 | import { getEnv, init } from "./utils/env.server";
10 |
11 | init();
12 | global.ENV = getEnv();
13 |
14 | export function handleError(
15 | error: unknown,
16 | { request }: LoaderFunctionArgs | ActionFunctionArgs,
17 | ) {
18 | if (!request.signal.aborted) {
19 | // If you want to log errors to an external service like Sentry, you can
20 | // Sentry.captureRemixServerException(error, "remix.server", request);
21 | console.error(error);
22 | }
23 | }
24 |
25 | export default async function handleRequest(
26 | request: Request,
27 | responseStatusCode: number,
28 | responseHeaders: Headers,
29 | remixContext: EntryContext,
30 | // This is ignored so we can keep it in the template for visibility. Feel
31 | // free to delete this parameter in your app if you're not using it!
32 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
33 | loadContext: AppLoadContext,
34 | ) {
35 | const remixServer = (
36 |
41 | );
42 | return vercelHandleRequest(
43 | request,
44 | responseStatusCode,
45 | responseHeaders,
46 | remixServer,
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/apps/web/app/hooks/use-abortable.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useRef } from "react";
2 |
3 | /**
4 | * Imperatively abort async functions;
5 | *
6 | * Source: [Kent Dodds](https://gist.github.com/kentcdodds/b36572b6e9227207e6c71fd80e63f3b4)
7 | *
8 | * @example
9 |
10 | const { getSignal, abortSignal } = useAbortController();
11 |
12 | const onRunQuery = useCallback(
13 | async (sql: string) => {
14 |
15 | dispatch({ type: "QUERY_START", payload: { sql } });
16 | try {
17 | const signal = getSignal();
18 | const doQuery = db.fetchResults({ query: sql });
19 |
20 | const isCancelledPromise = new Promise((_, reject) => {
21 | signal.addEventListener("abort", () => {
22 | reject(new DOMException("Aborted", "AbortError"));
23 | });
24 | });
25 |
26 | const results = await Promise.race([doQuery, isCancelledPromise]);
27 | const { rows, schema } = results as FetchResultsReturn;
28 | dispatch({ type: "QUERY_SUCCESS", payload: { rows, schema } });
29 |
30 | } catch (e) {
31 | if (e instanceof DOMException && e.name === "AbortError") {
32 | // We aborted the query
33 | }
34 | }
35 | },
36 | [db, getSignal],
37 | );
38 |
39 | */
40 | export default function useAbortController() {
41 | const abortControllerRef = useRef(null);
42 |
43 | const getAbortController = useCallback(() => {
44 | if (!abortControllerRef.current) {
45 | abortControllerRef.current = new AbortController();
46 | }
47 | return abortControllerRef.current;
48 | }, []);
49 |
50 | const abortSignal = useCallback((reason?: unknown) => {
51 | // if we don't have a current abort controller, then we don't have a current signal either...
52 | if (abortControllerRef.current) {
53 | abortControllerRef.current.abort(reason);
54 | abortControllerRef.current = null; // Resets it for next time
55 | }
56 | }, []);
57 |
58 | const getSignal = useCallback(
59 | () => getAbortController().signal,
60 | [getAbortController],
61 | );
62 |
63 | return { getSignal, abortSignal };
64 | }
65 |
--------------------------------------------------------------------------------
/apps/web/app/hooks/use-breakpoints.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export type Breakpoint = "sm" | "md" | "lg" | "xl" | "2xl";
4 |
5 | export const BREAKPOINTS: Record = {
6 | sm: 640,
7 | md: 768,
8 | lg: 1024,
9 | xl: 1280,
10 | "2xl": 1536,
11 | };
12 |
13 | // https://github.com/mfrkankaya/shadcn-themes/blob/main/hooks/use-breakpoint.ts
14 | export default function useBreakpoint(
15 | breakpoint: Breakpoint,
16 | direction: "up" | "down" = "down",
17 | ) {
18 | const [matches, setMatches] = React.useState(false);
19 |
20 | React.useEffect(() => {
21 | const mediaQuery = window.matchMedia(
22 | direction === "down"
23 | ? `(max-width: ${BREAKPOINTS[breakpoint]}px)`
24 | : `(min-width: ${BREAKPOINTS[breakpoint]}px)`,
25 | );
26 | setMatches(mediaQuery.matches);
27 |
28 | const handler = () => setMatches(mediaQuery.matches);
29 | mediaQuery.addEventListener("change", handler);
30 |
31 | return () => mediaQuery.removeEventListener("change", handler);
32 | }, [breakpoint, direction]);
33 |
34 | return matches;
35 | }
36 |
--------------------------------------------------------------------------------
/apps/web/app/hooks/use-copy-to-clipboard.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 |
3 | type UseCopyToClipboardProps = {
4 | timeout?: number;
5 | };
6 |
7 | export function useCopyToClipboard(props?: UseCopyToClipboardProps) {
8 | const timeout = props?.timeout ?? 1500;
9 | const [isCopied, setIsCopied] = useState(false);
10 |
11 | useEffect(() => {
12 | let timeoutId: NodeJS.Timeout | undefined;
13 | if (isCopied) {
14 | timeoutId = setTimeout(() => setIsCopied(false), timeout);
15 | }
16 | return () => clearTimeout(timeoutId);
17 | }, [isCopied, timeout]);
18 |
19 | const copyToClipboard = useCallback(async (value: string) => {
20 | if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
21 | return;
22 | }
23 |
24 | if (!value) return;
25 |
26 | await navigator.clipboard.writeText(value);
27 |
28 | setIsCopied(true);
29 | }, []);
30 |
31 | return { isCopied, copyToClipboard };
32 | }
33 |
--------------------------------------------------------------------------------
/apps/web/app/hooks/use-enter-submit.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, type RefObject, useCallback } from "react";
2 |
3 | /**
4 | *
5 | * @source https://github.com/vercel/ai-chatbot/blob/main/lib/hooks/use-enter-submit.tsx
6 | */
7 | export function useEnterSubmit(): {
8 | formRef: RefObject;
9 | onKeyDown: (event: React.KeyboardEvent) => void;
10 | } {
11 | const formRef = useRef(null);
12 |
13 | const handleKeyDown = useCallback(
14 | (event: React.KeyboardEvent): void => {
15 | if (
16 | event.key === "Enter" &&
17 | !event.shiftKey &&
18 | !event.nativeEvent.isComposing
19 | ) {
20 | formRef.current?.requestSubmit();
21 | event.preventDefault();
22 | }
23 | },
24 | [],
25 | );
26 |
27 | return { formRef, onKeyDown: handleKeyDown };
28 | }
29 |
--------------------------------------------------------------------------------
/apps/web/app/hooks/use-media-query.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | // https://github.com/shadcn-ui/ui/blob/main/apps/www/hooks/use-media-query.tsx
3 | export function useMediaQuery(query: string) {
4 | const [value, setValue] = React.useState(false)
5 |
6 | React.useEffect(() => {
7 | function onChange(event: MediaQueryListEvent) {
8 | setValue(event.matches)
9 | }
10 |
11 | const result = matchMedia(query)
12 | result.addEventListener("change", onChange)
13 | setValue(result.matches)
14 |
15 | return () => result.removeEventListener("change", onChange)
16 | }, [query])
17 |
18 | return value
19 | }
--------------------------------------------------------------------------------
/apps/web/app/hooks/use-mutation-selector.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | // https://github.com/shadcn-ui/ui/blob/main/apps/www/hooks/use-mutation-observer.ts
4 | export const useMutationObserver = (
5 | ref: React.MutableRefObject,
6 | callback: MutationCallback,
7 | options = {
8 | attributes: true,
9 | characterData: true,
10 | childList: true,
11 | subtree: true,
12 | }
13 | ) => {
14 | React.useEffect(() => {
15 | if (ref.current) {
16 | const observer = new MutationObserver(callback)
17 | observer.observe(ref.current, options)
18 | return () => observer.disconnect()
19 | }
20 | }, [ref, callback, options])
21 | }
--------------------------------------------------------------------------------
/apps/web/app/hooks/use-prettier-worker.tsx:
--------------------------------------------------------------------------------
1 | // import { useEffect, useRef, useState } from "react";
2 |
3 | // /**
4 | // * (Original inspiration](https://github.com/ritz078/transform/blob/c6e0748bad06a31373e2a8324a764e9467646742/components/ConversionPanel.tsx#L87C3-L117C49)
5 | // *
6 | // */
7 | // export const usePrettierWorker = () => {
8 | // const [isWorking, setIsWorking] = useState(false);
9 | // const prettierWorker = useRef(null);
10 |
11 | // useEffect(() => {
12 | // let worker: Worker | undefined;
13 | // return () => {
14 | // worker?.terminate();
15 | // };
16 | // }, []);
17 |
18 | // useEffect(() => {
19 | // async function transform() {
20 | // try {
21 | // setIsWorking(true);
22 | // prettierWorker = prettierWorker || getWorker(PrettierWorker);
23 |
24 | // const result = await transformer({
25 | // value,
26 | // splitEditorValue: splitTitle ? splitValue : undefined,
27 | // });
28 |
29 | // let prettyResult = await prettierWorker.send({
30 | // value: result,
31 | // language: resultLanguage,
32 | // });
33 |
34 | // // Fix for #319
35 | // if (prettyResult.startsWith(";<")) {
36 | // prettyResult = prettyResult.slice(1);
37 | // }
38 | // setResult(prettyResult);
39 | // setMessage("");
40 | // } catch (e) {
41 | // console.error(e);
42 | // setMessage(e.message);
43 | // }
44 | // toggleUpdateSpinner(false);
45 | // }
46 |
47 | // transform();
48 | // }, [splitValue, value, splitTitle, settings]);
49 | // };
50 |
--------------------------------------------------------------------------------
/apps/web/app/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/app/routes/$.tsx:
--------------------------------------------------------------------------------
1 | import { type MetaFunction } from "@vercel/remix";
2 | import { motion } from "framer-motion";
3 | import NavBar from "~/components/navbar";
4 | import ModeToggle from "~/components/theme-toggle";
5 | import { StyledLink } from "~/components/ui/link";
6 |
7 | export async function loader() {
8 | throw new Response("Not found", { status: 404 });
9 | }
10 |
11 | export const meta: MetaFunction = () => {
12 | return [
13 | {
14 | name: "robots",
15 | content: "noindex",
16 | },
17 | {
18 | title: "404 | QuackDB",
19 | },
20 | {
21 | name: "description",
22 | content: "404 | QuackDB",
23 | },
24 | {
25 | name: "og:title",
26 | content: "404 | QuackDB",
27 | },
28 | {
29 | name: "og:description",
30 | content: "404 | QuackDB",
31 | },
32 | ];
33 | };
34 |
35 | // Trigger error boundary from loader
36 | export function ErrorBoundary() {
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
53 |
54 | 404
55 |
56 |
57 | Page not found
58 |
59 |
60 | {`Sorry, we couldn't find the page you're looking for.`}
61 |
62 |
63 | Go back home
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | export default function Component() {
72 | return null;
73 | }
74 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/about.tsx:
--------------------------------------------------------------------------------
1 | import { ExternalLink, InfoIcon } from "lucide-react";
2 | import { Button } from "~/components/ui/button";
3 | import {
4 | Dialog,
5 | DialogClose,
6 | DialogContent,
7 | DialogDescription,
8 | DialogFooter,
9 | DialogHeader,
10 | DialogTitle,
11 | DialogTrigger,
12 | } from "~/components/ui/dialog";
13 | import { ScrollArea } from "~/components/ui/scroll-area";
14 |
15 | export default function AboutModal() {
16 | return (
17 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/editor-panel/components/open-files.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/interactive-supports-focus */
2 | /* eslint-disable jsx-a11y/click-events-have-key-events */
3 | import { Code2, Plus, X } from "lucide-react";
4 | import { useSession } from "~/context/session/useSession";
5 | import { cn } from "~/lib/utils";
6 |
7 | export default function OpenFileTabs() {
8 | const { editors, dispatch, onCloseEditor } = useSession();
9 |
10 | const onOpenEditor = (path: string) => {
11 | if (!dispatch) return;
12 |
13 | dispatch({
14 | type: "FOCUS_EDITOR",
15 | payload: {
16 | path,
17 | },
18 | });
19 | };
20 |
21 | return (
22 |
23 |
24 | {editors
25 | .filter((editor) => editor.isOpen)
26 | .map((editor) => {
27 | const isCurrent = editor.isFocused;
28 |
29 | return (
30 |
onOpenEditor(editor.path)}
40 | >
41 |
42 | {editor.path}
43 |
44 |
61 |
62 | );
63 | })}
64 |
65 |
68 |
69 |
70 | );
71 | }
72 |
73 | function AddNewFileButton() {
74 | const { onAddEditor } = useSession();
75 |
76 | return (
77 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/result-viewer/components/empty.tsx:
--------------------------------------------------------------------------------
1 | export default function EmptyResults() {
2 | return (
3 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/result-viewer/components/json-viewer/index.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense, lazy, memo, useCallback, useEffect, useMemo } from "react";
2 | import CopyToClipboard from "~/components/copy-to-clipboard";
3 | import PaginationToolbar from "~/components/paginator";
4 | import { ScrollArea } from "~/components/ui/scroll-area";
5 | import { usePagination } from "~/context/pagination/usePagination";
6 | import { useQuery } from "~/context/query/useQuery";
7 |
8 | const LazyShiki = lazy(() =>
9 | import("~/components/lazy-shiki").then((module) => ({
10 | default: module.default,
11 | })),
12 | );
13 |
14 | export const JSONViewer = memo(function JSONViewer() {
15 | const { table, count } = useQuery();
16 | const { limit, offset, onSetCount } = usePagination();
17 |
18 | // Update the count when we receive data (don't like this pattern...)
19 | // Effectively initializes the pagination state.
20 | useEffect(() => {
21 | onSetCount(count);
22 | }, [onSetCount, count]);
23 |
24 | const json = useMemo(() => {
25 | if (!table || table.numRows === 0) return "[]";
26 | const rows = table
27 | .slice(offset, offset + limit)
28 | .toArray()
29 | .map((row) => row.toJSON());
30 | return JSON.stringify(rows, null, 2);
31 | }, [table, offset, limit]);
32 |
33 | const lazyCopy = useCallback(() => {
34 | if (!table || table.numRows === 0) return "[]";
35 | const rows = table.toArray().map((row) => row.toJSON());
36 | return JSON.stringify(rows, null, 2);
37 | }, [table]);
38 |
39 | return (
40 |
41 |
42 |
43 |
47 |
48 |
49 |
50 |
51 |
52 |
55 |
56 | );
57 | });
58 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/result-viewer/components/table.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useEffect, useState } from "react";
2 | import DataGrid from "~/components/data-grid";
3 | import PaginationToolbar from "~/components/paginator";
4 | import { Label } from "~/components/ui/label";
5 | import { ScrollArea } from "~/components/ui/scroll-area";
6 | import { Switch } from "~/components/ui/switch";
7 | import VirtualizedGrid from "~/components/virtualized-grid";
8 | import { usePagination } from "~/context/pagination/usePagination";
9 | import { useQuery } from "~/context/query/useQuery";
10 | import EmptyResults from "./empty";
11 |
12 | export const TableViewer = memo(function TableViewer() {
13 | const [view, setView] = useState<"table" | "list">("table");
14 | const { table, meta, count } = useQuery();
15 |
16 | const { onSetCount } = usePagination();
17 |
18 | // Update the count when we receive data (don't like this pattern...)
19 | useEffect(() => {
20 | onSetCount(count);
21 | }, [onSetCount, count]);
22 |
23 | const noQuery = table.numRows === 0 && table.numCols === 0;
24 |
25 | return (
26 |
27 | {noQuery &&
}
28 |
29 | {view === "table" && (
30 |
35 | )}
36 | {view === "list" && }
37 |
38 |
39 |
40 | {
42 | setView(checked ? "list" : "table");
43 | }}
44 | id="beta-list"
45 | />
46 |
47 |
48 |
49 | {view === "table" &&
}
50 |
51 |
52 | );
53 | });
54 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/sidepanel/components/wrapper/context/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import type { WrapperState } from "./types";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 | export const WrapperContext = createContext(
6 | undefined,
7 | );
8 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/sidepanel/components/wrapper/context/provider.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo, useReducer, useRef } from "react";
2 | import { type ImperativePanelHandle } from "react-resizable-panels";
3 | import { WrapperContext } from "./context";
4 | import type { WrapperState } from "./types";
5 |
6 | type WrapperProviderProps = Pick & {
7 | children: React.ReactNode;
8 | };
9 |
10 | type State = Pick;
11 | type Action = {
12 | type: "TOGGLE_COLLAPSE";
13 | payload: {
14 | isCollapsed: boolean;
15 | };
16 | };
17 |
18 | function wrapperReducer(state: State, action: Action): State {
19 | switch (action.type) {
20 | case "TOGGLE_COLLAPSE":
21 | return {
22 | ...state,
23 | isCollapsed: action.payload.isCollapsed,
24 | };
25 | default:
26 | return { ...state };
27 | }
28 | }
29 |
30 | function WrapperProvider(props: WrapperProviderProps) {
31 | const ref = useRef(null);
32 |
33 | const [state, dispatch] = useReducer(wrapperReducer, {
34 | isCollapsed: false,
35 | });
36 |
37 | const onToggleIsCollapse = useCallback(
38 | (isCollapsed: boolean) =>
39 | dispatch({
40 | type: "TOGGLE_COLLAPSE",
41 | payload: {
42 | isCollapsed,
43 | },
44 | }),
45 | [],
46 | );
47 |
48 | const { order, id } = props;
49 |
50 | const value = useMemo(
51 | () => ({
52 | isCollapsed: state.isCollapsed,
53 | onToggleIsCollapse,
54 | ref,
55 | id,
56 | order,
57 | }),
58 | [onToggleIsCollapse, state.isCollapsed, id, order],
59 | );
60 |
61 | return (
62 |
63 | {props.children}
64 |
65 | );
66 | }
67 |
68 | export { WrapperProvider };
69 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/sidepanel/components/wrapper/context/types.ts:
--------------------------------------------------------------------------------
1 | import type { ImperativePanelHandle } from "react-resizable-panels";
2 |
3 | export type WrapperState = {
4 | isCollapsed: boolean;
5 | onToggleIsCollapse: (isCollapsed: boolean) => void;
6 | ref: React.RefObject;
7 | order: number;
8 | id: string;
9 | };
10 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/sidepanel/components/wrapper/context/useWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { WrapperContext } from "./context";
3 |
4 | // Breakup everything into smaller files because of React Fast Refresh limitations.
5 |
6 | /**
7 | * Hook to manage opening and closing panels.
8 | */
9 | export function useWrapper() {
10 | const context = useContext(WrapperContext);
11 | if (context === undefined) {
12 | throw new Error("useWrapper must be used within a WrapperContext");
13 | }
14 | return context;
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/sidepanel/components/wrapper/index.tsx:
--------------------------------------------------------------------------------
1 | import { Panel, type PanelProps } from "react-resizable-panels";
2 | import { cn } from "~/lib/utils";
3 | import { WrapperProvider } from "./context/provider";
4 | import type { WrapperState } from "./context/types";
5 | import { useWrapper } from "./context/useWrapper";
6 |
7 | type WrapperStateProps = Pick;
8 |
9 | type ComponentWrapperProps = PanelProps & {
10 | children: React.ReactNode;
11 | };
12 |
13 | export default function ComponentWrapper(
14 | props: ComponentWrapperProps & {
15 | wrapperState: WrapperStateProps;
16 | },
17 | ) {
18 | const { children, wrapperState, ...rest } = props;
19 | return (
20 |
21 | {children}
22 |
23 | );
24 | }
25 |
26 | /**
27 | * A wrapper component for the sidepanel components.
28 | *
29 | * OnCollapase occurs when the panel is resized to below the collapsedSize.
30 | * We also want to be able to opena and close the panel using our own chevron button in the header.
31 | * This is achieved by using the imperative api of the Panel component.
32 | * It is a bit confusing (see [docs](https://github.com/bvaughn/react-resizable-panels/issues/284)).
33 | *
34 | */
35 | function Content(props: ComponentWrapperProps) {
36 | const { ref, id, order, onToggleIsCollapse } = useWrapper();
37 | const { children, ...rest } = props;
38 |
39 | return (
40 | onToggleIsCollapse(true)}
49 | onExpand={() => onToggleIsCollapse(false)}
50 | className={cn("max-h-full", props.className)}
51 | {...rest}
52 | >
53 | {/* From the docs: Panels clip their content by default, to avoid showing scrollbars while resizing. Content can still be configured to overflow within a panel though.*/}
54 | {children}
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/components/sidepanel/index.tsx:
--------------------------------------------------------------------------------
1 | import { PanelGroup } from "react-resizable-panels";
2 | import PanelHandle from "~/components/panel-handle";
3 | import { useSession } from "~/context/session/useSession";
4 | import DataSources from "./components/data-sources";
5 | import EditorSources from "./components/editor-files";
6 | import QueryHistory from "./components/query-history";
7 | import ComponentWrapper from "./components/wrapper";
8 |
9 | type SidepanelProps = {
10 | isCollapsed: boolean;
11 | };
12 |
13 | /**
14 | * Left hand side panel which holds the data sources, editor sources and query history.
15 | *
16 | * NB: This panel controls the vertical resizing *within* the sidepanel.
17 | * The horizontal resizing between the side panel and the editor panel is in the parent panel.
18 | */
19 | export default function Sidepanel(props: SidepanelProps) {
20 | const { sessionId } = useSession();
21 | const { isCollapsed } = props;
22 |
23 | if (isCollapsed) return null;
24 |
25 | return (
26 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/apps/web/app/routes/_index/workers/is-supported.worker.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import * as Comlink from "comlink";
3 |
4 | /**
5 | * Check if the browser can transfer file system handles (WebKit has a bug with this).
6 | */
7 | async function isSupported() {
8 | try {
9 | const root = await navigator.storage.getDirectory();
10 | const handle = await root.getDirectoryHandle("__test__", { create: true });
11 | postMessage({
12 | type: "success",
13 | body: {
14 | handle,
15 | },
16 | });
17 |
18 | return true;
19 | } catch (e) {
20 | console.error("Error in worker: ", e);
21 | return false;
22 | }
23 | }
24 |
25 | export type IsSupportedWorker = typeof isSupported;
26 | Comlink.expose(isSupported);
27 |
--------------------------------------------------------------------------------
/apps/web/app/routes/action.set-theme.ts:
--------------------------------------------------------------------------------
1 | import { createThemeAction } from "remix-themes";
2 | import { themeSessionResolver } from "~/sessions.server";
3 |
4 | export const action = createThemeAction(themeSessionResolver);
5 |
--------------------------------------------------------------------------------
/apps/web/app/routes/error.tsx:
--------------------------------------------------------------------------------
1 | // Inspiration: https://github.com/kiliman/remix-vite-template/blob/main/app/routes/error.tsx
2 | import { Link } from "@remix-run/react";
3 | import { type MetaFunction } from "@vercel/remix";
4 | import ModeToggle from "~/components/theme-toggle";
5 | import { Button } from "~/components/ui/button";
6 | import * as Card from "~/components/ui/card";
7 |
8 | export const meta: MetaFunction = () => {
9 | // no index
10 | return [
11 | {
12 | name: "robots",
13 | content: "noindex",
14 | },
15 | {
16 | title: "Error | QuackDB",
17 | },
18 | {
19 | name: "description",
20 | content: "QuackDB error page for testing",
21 | },
22 | ];
23 | };
24 |
25 | export default function Component() {
26 | const handleClick = () => {
27 | setTimeout(() => alert("View console for error"), 1);
28 | throw new Error("test client error");
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 |
39 | Return Home
40 |
41 |
42 |
43 |
44 |
45 |
46 |
Error page for testing
47 |
48 |
49 | Throw an error
50 |
51 | This button will throw an error
52 |
53 |
54 |
55 |
75 |
76 |
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/apps/web/app/sessions.server.ts:
--------------------------------------------------------------------------------
1 | import { createCookieSessionStorage } from "@vercel/remix";
2 | import { createThemeSessionResolver } from "remix-themes";
3 |
4 | // You can default to 'development' if process.env.NODE_ENV is not set
5 | const isProduction = process.env.VERCEL === "1";
6 |
7 | const domain = isProduction
8 | ? process.env.VERCEL_PROJECT_PRODUCTION_URL
9 | : "localhost:3000";
10 |
11 | /**
12 | * Theme session storage
13 | */
14 | const sessionStorage = createCookieSessionStorage({
15 | cookie: {
16 | name: "theme",
17 | path: "/",
18 | httpOnly: true,
19 | sameSite: "lax",
20 | secrets: [process.env.SESSION_SECRET],
21 | ...(isProduction ? { domain, secure: true } : {}),
22 | },
23 | });
24 |
25 | export const themeSessionResolver = createThemeSessionResolver(sessionStorage);
26 |
--------------------------------------------------------------------------------
/apps/web/app/styles/dockview.css:
--------------------------------------------------------------------------------
1 | @import "dockview/dist/styles/dockview.css";
2 |
--------------------------------------------------------------------------------
/apps/web/app/types/files/code-source.ts:
--------------------------------------------------------------------------------
1 | // ---------- Code Ext files ----------- //
2 |
3 | /**
4 | * Only support sql for now (python, js, ts, rs are possible future additions).
5 | */
6 | export const codeFileExts = ["sql"] as const;
7 |
8 | type CodeFileExt = (typeof codeFileExts)[number];
9 |
10 | export function isCodeFileExt(x: unknown): x is CodeFileExt {
11 | return codeFileExts.includes(x as CodeFileExt);
12 | }
13 |
14 | // ------ Code Mime Types ------ //
15 | export const codeMimeTypes = ["text/sql"] as const;
16 |
17 | type CodeMimeType = (typeof codeMimeTypes)[number];
18 |
19 | export function isCodeMimeType(mimeType: unknown): mimeType is CodeMimeType {
20 | return codeMimeTypes.includes(mimeType as CodeMimeType);
21 | }
22 |
23 | export const codeExtMap: Record = {
24 | sql: "text/sql",
25 | };
26 |
27 | export type CodeSource = {
28 | kind: "CODE";
29 | mimeType: CodeMimeType;
30 | ext: CodeFileExt;
31 | handle: FileSystemFileHandle;
32 | path: string;
33 | };
34 |
--------------------------------------------------------------------------------
/apps/web/app/types/files/dataset.ts:
--------------------------------------------------------------------------------
1 | export const datasetFileExts = [
2 | "csv",
3 | "json",
4 | "txt",
5 | "duckdb",
6 | "sqlite",
7 | "postgresql",
8 | "parquet",
9 | "arrow",
10 | "excel",
11 | "url",
12 | ] as const;
13 |
14 | export type DatasetFileExt = (typeof datasetFileExts)[number];
15 |
16 | export function isDatasetFileExt(x: unknown): x is DatasetFileExt {
17 | return datasetFileExts.includes(x as DatasetFileExt);
18 | }
19 |
20 | export const datasetMimeTypes = [
21 | "text/csv",
22 | "application/json",
23 | "text/plain",
24 | "application/duckdb",
25 | "application/sqlite",
26 | "application/postgresql",
27 | "application/parquet",
28 | "application/arrow",
29 | "application/excel",
30 | "text/x-uri",
31 | ];
32 |
33 | // ------ Dataset Mime Types ------ //
34 |
35 | export type DatasetMimeType = (typeof datasetMimeTypes)[number];
36 |
37 | export function isDatasetMimeType(x: unknown): x is DatasetMimeType {
38 | return datasetMimeTypes.includes(x as DatasetMimeType);
39 | }
40 |
41 | export const datasetExtMap: Record = {
42 | csv: "text/csv",
43 | json: "application/json",
44 | txt: "text/plain",
45 | duckdb: "application/duckdb",
46 | sqlite: "application/sqlite",
47 | postgresql: "application/postgresql",
48 | parquet: "application/parquet",
49 | arrow: "application/arrow",
50 | excel: "application/excel",
51 | url: "text/x-uri", // remote sources
52 | };
53 |
54 | export type Dataset = {
55 | kind: "DATASET";
56 | mimeType: DatasetMimeType;
57 | ext: DatasetFileExt;
58 | handle: FileSystemFileHandle;
59 | path: string;
60 | };
61 |
--------------------------------------------------------------------------------
/apps/web/app/types/query.ts:
--------------------------------------------------------------------------------
1 | import { type Table } from "apache-arrow";
2 | import { z } from "zod";
3 |
4 | /**
5 | * Query meta Zod schema
6 | */
7 | export const queryMetaSchema = z.object({
8 | cacheHit: z.boolean(),
9 | executionTime: z.number(),
10 | sql: z.string(),
11 | error: z.string().nullable(),
12 | status: z.enum(["IDLE", "SUCCESS", "ERROR", "CANCELLED"]),
13 | hash: z.string(),
14 | created: z.string().datetime(),
15 | });
16 |
17 | /**
18 | * Store metadata about the query execution (WIP)
19 | */
20 | export type QueryMeta = z.infer;
21 |
22 | export type QueryResponse = {
23 | table: Table;
24 | meta: QueryMeta | null;
25 | count: number;
26 | };
27 |
--------------------------------------------------------------------------------
/apps/web/app/utils/arrow/helpers.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/observablehq/stdlib/blob/main/src/arrow.js
2 |
3 | import type { DataType, Field, RecordBatch, Table } from "apache-arrow";
4 |
5 | // Returns true if the vaue is an Apache Arrow table. This uses a “duck” test
6 | // (instead of strict instanceof) because we want it to work with a range of
7 | // Apache Arrow versions at least 7.0.0 or above.
8 | // https://arrow.apache.org/docs/7.0/js/classes/Arrow_dom.Table.html
9 |
10 | export function isArrowTable(value: unknown): value is Table {
11 | if (
12 | value &&
13 | typeof value === "object" &&
14 | "getChild" in value &&
15 | typeof value.getChild === "function" &&
16 | "toArray" in value &&
17 | typeof value.toArray === "function" &&
18 | "schema" in value &&
19 | value.schema &&
20 | typeof value.schema === "object" &&
21 | "fields" in value.schema &&
22 | Array.isArray(value.schema.fields)
23 | ) {
24 | return true;
25 | }
26 | return false;
27 | }
28 |
29 | /**
30 | * Returns the schema of an Apache Arrow table as an array of objects.
31 | */
32 | export function getArrowTableSchema(table: Table | RecordBatch) {
33 | return table.schema.fields.map(getArrowFieldSchema);
34 | }
35 |
36 | type JavaScriptArrowType =
37 | | "integer"
38 | | "number"
39 | | "buffer"
40 | | "string"
41 | | "boolean"
42 | | "date"
43 | | "array"
44 | | "object"
45 | | "other";
46 |
47 | export type ResultColumn = {
48 | name: string;
49 | type: JavaScriptArrowType;
50 | nullable: boolean;
51 | databaseType: string;
52 | };
53 |
54 | /**
55 | * Returns the schema of an Apache Arrow field as an object.
56 | */
57 | function getArrowFieldSchema(field: Field): ResultColumn {
58 | return {
59 | name: field.name,
60 | type: getArrowType(field.type),
61 | nullable: field.nullable,
62 | databaseType: String(field.type),
63 | };
64 | }
65 |
66 | // https://github.com/apache/arrow/blob/89f9a0948961f6e94f1ef5e4f310b707d22a3c11/js/src/enum.ts#L140-L141
67 |
68 | /**
69 | * Returns the type of an Apache Arrow field as a string.
70 | */
71 | export function getArrowType(type: DataType): JavaScriptArrowType {
72 | switch (type.typeId) {
73 | case 2: // Int
74 | return "integer";
75 | case 3: // Float
76 | case 7: // Decimal
77 | return "number";
78 | case 4: // Binary
79 | case 15: // FixedSizeBinary
80 | return "buffer";
81 | case 5: // Utf8
82 | return "string";
83 | case 6: // Bool
84 | return "boolean";
85 | case 8: // Date
86 | case 9: // Time
87 | case 10: // Timestamp
88 | return "date";
89 | case 12: // List
90 | case 16: // FixedSizeList
91 | return "array";
92 | case 13: // Struct
93 | case 14: // Union
94 | return "object";
95 | case 11: // Interval
96 | case 17: // Map
97 | default:
98 | return "other";
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/apps/web/app/utils/consume-readable.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/mckaywrigley/chatbot-ui/blob/main/lib/consume-stream.ts
2 |
3 | export async function consumeReadableStream(
4 | stream: ReadableStream,
5 | callback: (chunk: string) => void,
6 | signal: AbortSignal,
7 | ): Promise {
8 | const reader = stream.getReader();
9 | const decoder = new TextDecoder();
10 |
11 | signal.addEventListener("abort", () => reader.cancel(), { once: true });
12 |
13 | try {
14 | while (true) {
15 | const { done, value } = await reader.read();
16 |
17 | if (done) {
18 | break;
19 | }
20 |
21 | if (value) {
22 | callback(decoder.decode(value, { stream: true }));
23 | }
24 | }
25 | } catch (error) {
26 | if (signal.aborted) {
27 | console.error("Stream reading was aborted:", error);
28 | } else {
29 | console.error("Error consuming stream:", error);
30 | }
31 | } finally {
32 | reader.releaseLock();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/apps/web/app/utils/debounce.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | export function debounce any>(
3 | func: T,
4 | waitFor: number,
5 | ): T {
6 | let timeout: NodeJS.Timeout;
7 | return function (this: any, ...args: any[]) {
8 | clearTimeout(timeout);
9 | timeout = setTimeout(() => func.apply(this, args), waitFor);
10 | } as T;
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/app/utils/duckdb/helpers/columnMapper.ts:
--------------------------------------------------------------------------------
1 | import { type DuckDBInstance } from "~/modules/duckdb-singleton";
2 |
3 | /**
4 | * Return a mapping from column names to data types, or the empty Map if no such table columns exist.
5 | * @source
6 | * https://github.com/holdenmatt/duckdb-wasm-kit/blob/main/src/util/queries.ts
7 | */
8 | export async function columnMapper(
9 | query: DuckDBInstance["fetchResults"],
10 | name: string,
11 | ): Promise