@liqvid/studio 1.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/dist/esm/LiqvidDevToolsProvider.js +49 -0
- package/dist/esm/api/contract.mjs +48 -0
- package/dist/esm/api/project-meta.mjs +33 -0
- package/dist/esm/api/recording.mjs +156 -0
- package/dist/esm/api/root.mjs +4 -0
- package/dist/esm/api/static-file.mjs +82 -0
- package/dist/esm/api/types.mjs +1 -0
- package/dist/esm/client.mjs +98 -0
- package/dist/esm/conventions.mjs +3 -0
- package/dist/esm/index.mjs +20 -0
- package/dist/esm/initialize.mjs +50 -0
- package/dist/esm/jobs/watch-assets.mjs +99 -0
- package/dist/esm/jobs/watch-project-files.mjs +216 -0
- package/dist/esm/next/api.mjs +48 -0
- package/dist/esm/next/page.js +15 -0
- package/dist/esm/pages/NewProjectButton.js +62 -0
- package/dist/esm/pages/RebuildButton.js +24 -0
- package/dist/esm/pages/root-actions.js +151 -0
- package/dist/esm/pages/root.js +25 -0
- package/dist/esm/pages/root.module.css +326 -0
- package/dist/esm/palette.css +279 -0
- package/dist/esm/providers/hosting/github-pages.mjs +10 -0
- package/dist/esm/providers/hosting/liqvid-studio.mjs +8 -0
- package/dist/esm/providers/hosting/s3.mjs +9 -0
- package/dist/esm/providers/hosting/sftp.mjs +22 -0
- package/dist/esm/providers/index.mjs +10 -0
- package/dist/esm/providers/social/bluesky.mjs +8 -0
- package/dist/esm/providers/social/facebook.mjs +8 -0
- package/dist/esm/providers/social/instagram.mjs +8 -0
- package/dist/esm/providers/social/twitter.mjs +7 -0
- package/dist/esm/providers/social/youtube.mjs +7 -0
- package/dist/esm/providers/types.mjs +1 -0
- package/dist/esm/publish.mjs +37 -0
- package/dist/esm/recording/RecordingControl.js +110 -0
- package/dist/esm/recording/RecordingControl.module.css +0 -0
- package/dist/esm/recording/RecordingDialog.js +114 -0
- package/dist/esm/recording/RecordingDialog.module.css +194 -0
- package/dist/esm/schemas/liqvid-config.mjs +32 -0
- package/dist/esm/schemas/project.mjs +27 -0
- package/dist/esm/schemas/recording-meta.mjs +11 -0
- package/dist/esm/types/assets.mjs +1 -0
- package/dist/esm/types.mjs +12 -0
- package/dist/esm/ui/Dialog.js +71 -0
- package/dist/esm/ui/DockableDialog.js +131 -0
- package/dist/esm/ui/DockableDialog.module.css +63 -0
- package/dist/esm/ui/RadioTabs.js +13 -0
- package/dist/esm/ui/RadioTabs.module.css +54 -0
- package/dist/esm/ui/Tabs.js +29 -0
- package/dist/esm/ui/Tabs.module.css +31 -0
- package/dist/esm/ui/Toast.js +64 -0
- package/dist/esm/ui/Toast.module.css +50 -0
- package/dist/esm/ui/Toaster.js +13 -0
- package/dist/esm/ui/Toaster.module.css +9 -0
- package/dist/esm/ui/test.js +14 -0
- package/dist/esm/utils/dom.mjs +6 -0
- package/dist/esm/utils/fs.mjs +94 -0
- package/dist/esm/utils/misc.mjs +15 -0
- package/dist/esm/utils/react.mjs +8 -0
- package/dist/esm/utils/rsync.mjs +57 -0
- package/dist/templates/project.json.hbs +5 -0
- package/dist/templates/projects/code/page.tsx.hbs +23 -0
- package/dist/templates/projects/code/src/assets.ts.hbs +9 -0
- package/dist/templates/projects/code/src/client.tsx.hbs +22 -0
- package/dist/templates/projects/code/src/helpers.ts.hbs +21 -0
- package/dist/templates/projects/code/src/highlights.ts.hbs +3 -0
- package/dist/templates/projects/code/src/markers.ts.hbs +13 -0
- package/dist/templates/projects/code/src/project.ts.hbs +23 -0
- package/dist/templates/projects/code/template.json +3 -0
- package/dist/templates/projects/default/page.tsx.hbs +23 -0
- package/dist/templates/projects/default/src/assets.ts.hbs +9 -0
- package/dist/templates/projects/default/src/client.tsx.hbs +22 -0
- package/dist/templates/projects/default/src/helpers.ts.hbs +21 -0
- package/dist/templates/projects/default/src/highlights.ts.hbs +3 -0
- package/dist/templates/projects/default/src/markers.ts.hbs +13 -0
- package/dist/templates/projects/default/src/project.ts.hbs +23 -0
- package/dist/templates/projects/default/template.json +4 -0
- package/dist/templates/types.ts.hbs +20 -0
- package/dist/types/LiqvidDevToolsProvider.d.ts +12 -0
- package/dist/types/api/contract.d.mts +66 -0
- package/dist/types/api/project-meta.d.mts +1 -0
- package/dist/types/api/recording.d.mts +5 -0
- package/dist/types/api/root.d.mts +1 -0
- package/dist/types/api/static-file.d.mts +6 -0
- package/dist/types/api/types.d.mts +1 -0
- package/dist/types/client.d.mts +43 -0
- package/dist/types/conventions.d.mts +3 -0
- package/dist/types/index.d.mts +19 -0
- package/dist/types/initialize.d.mts +14 -0
- package/dist/types/jobs/watch-assets.d.mts +18 -0
- package/dist/types/jobs/watch-project-files.d.mts +4 -0
- package/dist/types/next/api.d.mts +14 -0
- package/dist/types/next/page.d.ts +4 -0
- package/dist/types/pages/NewProjectButton.d.ts +1 -0
- package/dist/types/pages/RebuildButton.d.ts +1 -0
- package/dist/types/pages/root-actions.d.ts +29 -0
- package/dist/types/pages/root.d.ts +1 -0
- package/dist/types/providers/hosting/github-pages.d.mts +11 -0
- package/dist/types/providers/hosting/liqvid-studio.d.mts +10 -0
- package/dist/types/providers/hosting/s3.d.mts +11 -0
- package/dist/types/providers/hosting/sftp.d.mts +13 -0
- package/dist/types/providers/index.d.mts +10 -0
- package/dist/types/providers/social/bluesky.d.mts +10 -0
- package/dist/types/providers/social/facebook.d.mts +10 -0
- package/dist/types/providers/social/instagram.d.mts +10 -0
- package/dist/types/providers/social/twitter.d.mts +9 -0
- package/dist/types/providers/social/youtube.d.mts +9 -0
- package/dist/types/providers/types.d.mts +9 -0
- package/dist/types/publish.d.mts +1 -0
- package/dist/types/recording/RecordingControl.d.ts +16 -0
- package/dist/types/recording/RecordingDialog.d.ts +10 -0
- package/dist/types/schemas/liqvid-config.d.mts +51 -0
- package/dist/types/schemas/project.d.mts +51 -0
- package/dist/types/schemas/recording-meta.d.mts +17 -0
- package/dist/types/types/assets.d.mts +3 -0
- package/dist/types/types.d.mts +20 -0
- package/dist/types/ui/Dialog.d.ts +31 -0
- package/dist/types/ui/DockableDialog.d.ts +25 -0
- package/dist/types/ui/RadioTabs.d.ts +18 -0
- package/dist/types/ui/Tabs.d.ts +9 -0
- package/dist/types/ui/Toast.d.ts +23 -0
- package/dist/types/ui/Toaster.d.ts +6 -0
- package/dist/types/ui/test.d.ts +9 -0
- package/dist/types/utils/dom.d.mts +3 -0
- package/dist/types/utils/fs.d.mts +32 -0
- package/dist/types/utils/misc.d.mts +4 -0
- package/dist/types/utils/react.d.mts +5 -0
- package/dist/types/utils/rsync.d.mts +10 -0
- package/package.json +94 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useColorScheme } from "@liqvid/color-scheme/react";
|
|
4
|
+
import { useKeyboardShortcut } from "@liqvid/keymap/react";
|
|
5
|
+
import { onClickReact, onDragReact } from "@liqvid/utils";
|
|
6
|
+
import { Portal } from "@radix-ui/react-portal";
|
|
7
|
+
import classNames from "classnames";
|
|
8
|
+
import { Children, cloneElement, createContext, isValidElement, useContext, useEffect, useMemo, useRef, } from "react";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { useToggle } from "../utils/react.mjs";
|
|
11
|
+
import styles from "./DockableDialog.module.css";
|
|
12
|
+
/**
|
|
13
|
+
* Merges props onto a single React child element.
|
|
14
|
+
* Similar to Radix's Slot component.
|
|
15
|
+
*/
|
|
16
|
+
function Slot({ children, className, ...props }) {
|
|
17
|
+
const child = Children.only(children);
|
|
18
|
+
if (!isValidElement(child)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const childProps = child.props;
|
|
22
|
+
return cloneElement(child, {
|
|
23
|
+
...props,
|
|
24
|
+
className: classNames(className, childProps.className),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const dockableDialogContext = createContext({
|
|
28
|
+
open: false,
|
|
29
|
+
setOpen() { },
|
|
30
|
+
toggle() { },
|
|
31
|
+
});
|
|
32
|
+
dockableDialogContext.displayName = "DockableDialog";
|
|
33
|
+
function useDockableDialogState() {
|
|
34
|
+
return useContext(dockableDialogContext);
|
|
35
|
+
}
|
|
36
|
+
const openKey = `lv-dockable-dialog-open.`;
|
|
37
|
+
const positionKey = `lv-dockable-dialog-position.`;
|
|
38
|
+
function Root({ children, name, shortcut, }) {
|
|
39
|
+
const { value: open, set: setOpen, toggle } = useToggle();
|
|
40
|
+
useKeyboardShortcut(shortcut, toggle);
|
|
41
|
+
// persist open state
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!name)
|
|
44
|
+
return;
|
|
45
|
+
const sessionValue = window.sessionStorage.getItem(openKey + name);
|
|
46
|
+
if (sessionValue === "true")
|
|
47
|
+
setOpen(true);
|
|
48
|
+
}, [name, setOpen]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!name)
|
|
51
|
+
return;
|
|
52
|
+
window.sessionStorage.setItem(openKey + name, String(open));
|
|
53
|
+
}, [open, name]);
|
|
54
|
+
// context value
|
|
55
|
+
const context = useMemo(() => ({ name, open, setOpen, toggle }), [name, open, toggle, setOpen]);
|
|
56
|
+
return (_jsx(dockableDialogContext.Provider, { value: context, children: children }));
|
|
57
|
+
}
|
|
58
|
+
function Trigger({ asChild = false, children, }) {
|
|
59
|
+
const { toggle } = useDockableDialogState();
|
|
60
|
+
const Component = asChild ? Slot : "button";
|
|
61
|
+
const events = useMemo(() => onClickReact(toggle), [toggle]);
|
|
62
|
+
return _jsx(Component, { ...events, children: children });
|
|
63
|
+
}
|
|
64
|
+
function Content({ asChild = false, className, ...props }) {
|
|
65
|
+
const Component = asChild ? Slot : "div";
|
|
66
|
+
return (_jsx(Component, { className: classNames(styles.DockableDialogContent, className), ...props }));
|
|
67
|
+
}
|
|
68
|
+
function Header({ asChild = false, className, ...props }) {
|
|
69
|
+
const { name } = useDockableDialogState();
|
|
70
|
+
const Component = asChild ? Slot : "header";
|
|
71
|
+
const ref = useRef(null);
|
|
72
|
+
const offset = useRef([0, 0]);
|
|
73
|
+
const events = useMemo(() => onDragReact((_e, { x, y }) => {
|
|
74
|
+
const parent = ref.current?.parentElement;
|
|
75
|
+
if (!parent)
|
|
76
|
+
return;
|
|
77
|
+
const [offsetX, offsetY] = offset.current;
|
|
78
|
+
Object.assign(parent.style, {
|
|
79
|
+
translate: `${x + offsetX}px ${y + offsetY}px`,
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
// down
|
|
83
|
+
(_e, { x, y }) => {
|
|
84
|
+
if (!ref.current)
|
|
85
|
+
return;
|
|
86
|
+
const rect = ref.current.getBoundingClientRect();
|
|
87
|
+
offset.current = [rect.x - x, rect.y - y];
|
|
88
|
+
},
|
|
89
|
+
// up
|
|
90
|
+
(_e, { x, y }) => {
|
|
91
|
+
if (!name)
|
|
92
|
+
return;
|
|
93
|
+
window.sessionStorage.setItem(positionKey + name, `${offset.current[0] + x} ${offset.current[1] + y}`);
|
|
94
|
+
}), [name]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!name)
|
|
97
|
+
return;
|
|
98
|
+
const parent = ref.current?.parentElement;
|
|
99
|
+
if (!parent)
|
|
100
|
+
return;
|
|
101
|
+
// get saved value
|
|
102
|
+
const SavedCoordinates = z
|
|
103
|
+
.templateLiteral([z.number(), " ", z.number()])
|
|
104
|
+
.transform((arg) => arg.split(" ").map((x) => parseFloat(x)));
|
|
105
|
+
const savedRaw = window.sessionStorage.getItem(positionKey + name);
|
|
106
|
+
const $saved = SavedCoordinates.safeParse(savedRaw);
|
|
107
|
+
if (!$saved.success)
|
|
108
|
+
return;
|
|
109
|
+
// restore saved value
|
|
110
|
+
const [savedX, savedY] = $saved.data;
|
|
111
|
+
Object.assign(parent.style, {
|
|
112
|
+
translate: `${savedX}px ${savedY}px`,
|
|
113
|
+
});
|
|
114
|
+
}, [name]);
|
|
115
|
+
return (_jsx(Component, { className: classNames(styles.DockableDialogHeader, className), ref: ref, ...events, ...props }));
|
|
116
|
+
}
|
|
117
|
+
function Dialog({ className, ...props }) {
|
|
118
|
+
const { open } = useDockableDialogState();
|
|
119
|
+
const { colorScheme } = useColorScheme();
|
|
120
|
+
return (_jsx(Portal, { children: _jsx("aside", { className: classNames(styles.DockableDialog, "shadow-lg", className), hidden: !open, style: {
|
|
121
|
+
colorScheme,
|
|
122
|
+
}, ...props }) }));
|
|
123
|
+
}
|
|
124
|
+
export const DockableDialog = {
|
|
125
|
+
Content,
|
|
126
|
+
Dialog,
|
|
127
|
+
Header,
|
|
128
|
+
Portal,
|
|
129
|
+
Root,
|
|
130
|
+
Trigger,
|
|
131
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
@layer base {
|
|
2
|
+
html {
|
|
3
|
+
color-scheme: light dark;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
#lv-recording {
|
|
8
|
+
position: relative;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* #lv-recording-dialog { */
|
|
12
|
+
/* background-color: #2a2a2a; */
|
|
13
|
+
/* border-radius: 2px 2px 0 0; */
|
|
14
|
+
/* box-shadow: 2px -2px 2px 2px rgba(0, 0, 0, 0.3); */
|
|
15
|
+
/* box-sizing: border-box; */
|
|
16
|
+
/* color: #fff; */
|
|
17
|
+
/* font-family: sans-serif; */
|
|
18
|
+
/* line-height: 1; */
|
|
19
|
+
/* position: absolute; */
|
|
20
|
+
/* bottom: calc(var(--lv-controls-height) - 2px); */
|
|
21
|
+
/* right: 0; */
|
|
22
|
+
/* z-index: 3; */
|
|
23
|
+
/* max-height: 20rem; */
|
|
24
|
+
/* overflow-y: auto; */
|
|
25
|
+
/* padding: 0.5em; */
|
|
26
|
+
/* width: 23rem; */
|
|
27
|
+
/* > h3 { */
|
|
28
|
+
/* color: #1a69b5; */
|
|
29
|
+
/* margin: 0.5em 0 0.2em; */
|
|
30
|
+
/* } */
|
|
31
|
+
/* } */
|
|
32
|
+
|
|
33
|
+
.DockableDialog {
|
|
34
|
+
--border-radius: 4px;
|
|
35
|
+
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
width: 500px;
|
|
39
|
+
position: absolute;
|
|
40
|
+
|
|
41
|
+
* {
|
|
42
|
+
transition-property: background-color, color;
|
|
43
|
+
transition-timing-function: var(--default-transition-timing-function);
|
|
44
|
+
transition-duration: 150ms;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.DockableDialogHeader {
|
|
49
|
+
background-color: var(--accent-solid);
|
|
50
|
+
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
|
51
|
+
color: #fff;
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
font-weight: bold;
|
|
54
|
+
padding: 4px 8px;
|
|
55
|
+
user-select: none;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.DockableDialogContent {
|
|
59
|
+
background-color: light-dark(#e0e0e0, #333);
|
|
60
|
+
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
|
61
|
+
color: light-dark(#000, #fff);
|
|
62
|
+
padding: 8px 16px;
|
|
63
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Radio } from "@base-ui/react/radio";
|
|
4
|
+
import { RadioGroup } from "@base-ui/react/radio-group";
|
|
5
|
+
import classNames from "classnames";
|
|
6
|
+
import styles from "./RadioTabs.module.css";
|
|
7
|
+
function RadioTabs({ className, value, onValueChange, ...props }) {
|
|
8
|
+
return (_jsx(RadioGroup, { className: classNames(styles.RadioTabs, className), onValueChange: onValueChange, value: value, ...props }));
|
|
9
|
+
}
|
|
10
|
+
function RadioTabsItem({ className, icon: IconComponent, iconSize = 18, title, value, ...props }) {
|
|
11
|
+
return (_jsxs(Radio.Root, { className: classNames(styles.RadioTabsItem, className), title: title, value: value, ...props, children: [_jsx(IconComponent, { className: styles.iconRegular, size: iconSize }), _jsx(IconComponent, { className: styles.iconFill, size: iconSize, weight: "fill" })] }));
|
|
12
|
+
}
|
|
13
|
+
export { RadioTabs, RadioTabsItem };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
.RadioTabs {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
background-color: var(--gray-ui);
|
|
4
|
+
border-radius: 6px;
|
|
5
|
+
padding: 3px;
|
|
6
|
+
gap: 2px;
|
|
7
|
+
width: max-content;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.RadioTabsItem {
|
|
11
|
+
align-items: center;
|
|
12
|
+
background-color: transparent;
|
|
13
|
+
border: none;
|
|
14
|
+
border-radius: 4px;
|
|
15
|
+
color: var(--gray-solid);
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
display: flex;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
padding: 6px 10px;
|
|
20
|
+
transition:
|
|
21
|
+
background-color 0.15s,
|
|
22
|
+
color 0.15s;
|
|
23
|
+
|
|
24
|
+
&:hover {
|
|
25
|
+
background-color: var(--gray-hover);
|
|
26
|
+
color: var(--gray-normal);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&[data-checked] {
|
|
30
|
+
background-color: var(--accent-solid);
|
|
31
|
+
color: var(--accent-contrast);
|
|
32
|
+
|
|
33
|
+
.iconRegular {
|
|
34
|
+
display: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.iconFill {
|
|
38
|
+
display: block;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&:focus-visible {
|
|
43
|
+
outline: 2px solid var(--accent-focus);
|
|
44
|
+
outline-offset: 1px;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.iconRegular {
|
|
49
|
+
display: block;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.iconFill {
|
|
53
|
+
display: none;
|
|
54
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
4
|
+
import classNames from "classnames";
|
|
5
|
+
import { Children, cloneElement, isValidElement } from "react";
|
|
6
|
+
import styles from "./Tabs.module.css";
|
|
7
|
+
function Tabs({ className, ...props }) {
|
|
8
|
+
return (_jsx(TabsPrimitive.Root, { className: classNames("flex flex-col gap-2", className), "data-slot": "tabs", ...props }));
|
|
9
|
+
}
|
|
10
|
+
function TabsList({ className, ...props }) {
|
|
11
|
+
return (_jsx(TabsPrimitive.List, { className: classNames(styles.TabsList, "inline-flex w-fit items-center justify-center rounded-lg bg-muted p-[3px]", className), "data-slot": "tabs-list", ...props }));
|
|
12
|
+
}
|
|
13
|
+
function TabsTrigger({ className, ...props }) {
|
|
14
|
+
return (_jsx(TabsPrimitive.Tab, { className: classNames(styles.TabsTrigger, "inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap border border-transparent font-medium text-sm transition-[color,box-shadow] focus-visible:border-ring focus-visible:outline-1 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", className), "data-slot": "tabs-trigger", ...props }));
|
|
15
|
+
}
|
|
16
|
+
function TabsContent({ asChild, children, className, ...props }) {
|
|
17
|
+
const combinedClassName = classNames("flex-1 outline-none", className);
|
|
18
|
+
if (asChild && isValidElement(children)) {
|
|
19
|
+
return (_jsx(TabsPrimitive.Panel, { ...props, render: (renderProps) => {
|
|
20
|
+
const child = Children.only(children);
|
|
21
|
+
return cloneElement(child, {
|
|
22
|
+
...renderProps,
|
|
23
|
+
className: classNames(combinedClassName, child.props.className),
|
|
24
|
+
});
|
|
25
|
+
} }));
|
|
26
|
+
}
|
|
27
|
+
return (_jsx(TabsPrimitive.Panel, { className: combinedClassName, "data-slot": "tabs-content", ...props, children: children }));
|
|
28
|
+
}
|
|
29
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.TabsList {
|
|
2
|
+
height: min-content;
|
|
3
|
+
margin: 0 auto;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.TabsTrigger {
|
|
7
|
+
background-color: light-dark(#0002, #fff2);
|
|
8
|
+
color: #fff;
|
|
9
|
+
font-family: "Inter Variable", sans-serif;
|
|
10
|
+
font-weight: 500;
|
|
11
|
+
font-size: 14px;
|
|
12
|
+
padding: 1px 8px;
|
|
13
|
+
|
|
14
|
+
&[disabled] {
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
opacity: 50%;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&[data-active] {
|
|
20
|
+
background-color: var(--accent-solid);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&:first-child {
|
|
24
|
+
border-radius: 4px 0 0 4px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&:last-child {
|
|
28
|
+
border-radius: 0 4px 4px 0;
|
|
29
|
+
}
|
|
30
|
+
/* data-[state=active]:bg-background data-[state=active]:shadow-sm dark:text-muted-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground */
|
|
31
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CheckCircleIcon, IconContext, InfoIcon, XCircleIcon, } from "@phosphor-icons/react";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
import styles from "./Toast.module.css";
|
|
6
|
+
const icons = {
|
|
7
|
+
info: _jsx(XCircleIcon, { color: "red", weight: "fill" }),
|
|
8
|
+
negative: _jsx(InfoIcon, { color: "slateblue", weight: "fill" }),
|
|
9
|
+
success: _jsx(CheckCircleIcon, { color: "green", weight: "fill" }),
|
|
10
|
+
};
|
|
11
|
+
export function Toast({ className, message, ref, title, type: toastType = "info", }) {
|
|
12
|
+
// hide animation
|
|
13
|
+
const elt = useRef(null);
|
|
14
|
+
// Add appear animation on mount
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!elt.current)
|
|
17
|
+
return;
|
|
18
|
+
elt.current.animate(appearToast.keyframes, appearToast.options);
|
|
19
|
+
}, []);
|
|
20
|
+
// useImperativeHandle(ref, () => ({
|
|
21
|
+
// hide(opts = {}) {
|
|
22
|
+
// return new Promise<void>((resolve) => {
|
|
23
|
+
// if (!elt.current) return;
|
|
24
|
+
//
|
|
25
|
+
// const anim = elt.current.animate(hideToast.keyframes, {
|
|
26
|
+
// ...hideToast.options,
|
|
27
|
+
// ...opts,
|
|
28
|
+
// });
|
|
29
|
+
// anim.addEventListener("finish", () => resolve());
|
|
30
|
+
// });
|
|
31
|
+
// },
|
|
32
|
+
// }));
|
|
33
|
+
const icon = icons[toastType];
|
|
34
|
+
return (_jsxs("aside", { className: classNames(styles.Toast, className), onClick: (e) => e.stopPropagation(), ref: elt, children: [_jsx("div", { className: styles.icon, children: _jsx(IconContext.Provider, { value: { height: "100%", width: "100%" }, children: icon }) }), _jsx("header", { children: title }), message && _jsx("div", { className: styles.message, children: message })] }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Animation for hiding a toast notification.
|
|
38
|
+
* @see {@link https://www.figma.com/file/zdML9fFY9V0Oah28S0Msd3?node-id=3384:30299#314140994 Figma discussion}
|
|
39
|
+
*/
|
|
40
|
+
export const hideToast = {
|
|
41
|
+
keyframes: [
|
|
42
|
+
{ opacity: "1", transform: "translateY(0%)" },
|
|
43
|
+
{ opacity: "0", transform: "translateY(calc(100% + 1em))" },
|
|
44
|
+
],
|
|
45
|
+
options: {
|
|
46
|
+
duration: 200,
|
|
47
|
+
easing: "ease-out",
|
|
48
|
+
fill: "forwards",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Animation for showing a toast notification.
|
|
53
|
+
*/
|
|
54
|
+
export const appearToast = {
|
|
55
|
+
keyframes: [
|
|
56
|
+
{ opacity: "0", transform: "translateY(calc(100% + 1em))" },
|
|
57
|
+
{ opacity: "1", transform: "translateY(0%)" },
|
|
58
|
+
],
|
|
59
|
+
options: {
|
|
60
|
+
duration: 200,
|
|
61
|
+
easing: "ease-out",
|
|
62
|
+
fill: "forwards",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
.Toast {
|
|
2
|
+
background-color: var(--gray-app);
|
|
3
|
+
border-color: var(--gray-sep);
|
|
4
|
+
border-radius: 8px;
|
|
5
|
+
box-shadow:
|
|
6
|
+
0px 1px 2px 0px light-dark(#00000026, #33333326),
|
|
7
|
+
0px 3px 7px 0px light-dark(#00000040, #33333340);
|
|
8
|
+
color: light-dark(#000, #fff);
|
|
9
|
+
font-size: 14px;
|
|
10
|
+
padding: 8px;
|
|
11
|
+
|
|
12
|
+
transition-property: background-color, color;
|
|
13
|
+
/* transition-timing-function: var(--default-transition-timing-function); */
|
|
14
|
+
transition-duration: 150ms;
|
|
15
|
+
|
|
16
|
+
--icon-size: 24px;
|
|
17
|
+
--icon-offset: 8px;
|
|
18
|
+
|
|
19
|
+
/* layout */
|
|
20
|
+
display: grid;
|
|
21
|
+
grid:
|
|
22
|
+
"icon header"
|
|
23
|
+
"icon message" / calc(var(--icon-size) + var(--icon-offset)) auto auto;
|
|
24
|
+
position: relative;
|
|
25
|
+
width: 300px;
|
|
26
|
+
|
|
27
|
+
/* title */
|
|
28
|
+
> header {
|
|
29
|
+
color: var(--gray-normal);
|
|
30
|
+
font-family: "Inter", sans-serif;
|
|
31
|
+
font-size: 16px;
|
|
32
|
+
font-weight: 500;
|
|
33
|
+
line-height: var(--icon-size);
|
|
34
|
+
|
|
35
|
+
grid-area: header;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.message {
|
|
40
|
+
color: var(--gray-dim);
|
|
41
|
+
|
|
42
|
+
grid-area: message;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.icon {
|
|
46
|
+
height: var(--icon-size);
|
|
47
|
+
width: var(--icon-size);
|
|
48
|
+
|
|
49
|
+
grid-area: icon;
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useColorScheme } from "@liqvid/color-scheme/react";
|
|
3
|
+
import { HydrateElement } from "@liqvid/hydration";
|
|
4
|
+
import { Toast } from "./Toast.js";
|
|
5
|
+
import styles from "./Toaster.module.css";
|
|
6
|
+
export function Toaster({ toasts, }) {
|
|
7
|
+
const { colorScheme, persistence } = useColorScheme();
|
|
8
|
+
const inner = (_jsx("div", { className: styles.Toaster, style: { colorScheme }, children: toasts.map((t) => (_jsx(Toast, { ...t }, t.time))) }));
|
|
9
|
+
return persistence ? (_jsx(HydrateElement, { from: [persistence], hydrationFn: (node, colorScheme) => {
|
|
10
|
+
// node.style.
|
|
11
|
+
node.setAttribute("style", `color-scheme:${colorScheme}`);
|
|
12
|
+
}, children: inner })) : (inner);
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo } from "react";
|
|
3
|
+
const DialogContext = createContext({
|
|
4
|
+
min: 0,
|
|
5
|
+
});
|
|
6
|
+
export function useDialog() {
|
|
7
|
+
return useContext(DialogContext);
|
|
8
|
+
}
|
|
9
|
+
export function DialogProvider({ children }) {
|
|
10
|
+
const context = useMemo(() => ({
|
|
11
|
+
min: 0,
|
|
12
|
+
}), []);
|
|
13
|
+
return (_jsx(DialogContext.Provider, { value: context, children: children }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import * as fsp from "node:fs/promises";
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { Err, Maybe, safeJsonParse } from "have-fun";
|
|
6
|
+
import { fromZod } from "have-fun/zod";
|
|
7
|
+
export async function loadJson(Model, filename) {
|
|
8
|
+
try {
|
|
9
|
+
const file = await fsp.readFile(filename, "utf8");
|
|
10
|
+
return safeJsonParse(file).flatMap((json) => fromZod(Model.safeParse(json)));
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
return Err(err);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function findUpwards(dirname, callback) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const checkDirectory = async (dir) => {
|
|
19
|
+
try {
|
|
20
|
+
// Check if current directory matches callback
|
|
21
|
+
const result = await Promise.resolve(callback(dir));
|
|
22
|
+
if (result) {
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
// Get parent directory
|
|
26
|
+
const parentDir = path.dirname(dir);
|
|
27
|
+
// If we've reached the root directory, stop searching
|
|
28
|
+
if (parentDir === dir) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
// Continue searching upwards
|
|
32
|
+
return await checkDirectory(parentDir);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
reject(error);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
checkDirectory(dirname).then(resolve).catch(reject);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Walk a directory recursively */
|
|
43
|
+
export async function walkDir(
|
|
44
|
+
/** Directory to walk */
|
|
45
|
+
dir,
|
|
46
|
+
/** Callback to run for each file */
|
|
47
|
+
callback,
|
|
48
|
+
/** Callback to decide whether to descend into a directory */
|
|
49
|
+
shouldProcessDir = () => true) {
|
|
50
|
+
const files = await readdir(dir, { withFileTypes: true });
|
|
51
|
+
await Promise.all(files.map(async (dirent) => {
|
|
52
|
+
const qualified = path.join(dirent.parentPath, dirent.name);
|
|
53
|
+
if (dirent.isDirectory()) {
|
|
54
|
+
if (shouldProcessDir({ basename: dirent.name, dirname: qualified })) {
|
|
55
|
+
await walkDir(qualified, callback, shouldProcessDir);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (dirent.isFile()) {
|
|
59
|
+
await callback({
|
|
60
|
+
basename: dirent.name,
|
|
61
|
+
dirname: dirent.parentPath,
|
|
62
|
+
filename: qualified,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
/** Synchronously walk a directory recursively */
|
|
68
|
+
export function walkDirSync(
|
|
69
|
+
/** Directory to walk */
|
|
70
|
+
dir,
|
|
71
|
+
/** Callback to call for each file */
|
|
72
|
+
callback) {
|
|
73
|
+
const files = readdirSync(dir);
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const qualified = path.join(dir, file);
|
|
76
|
+
const stats = statSync(qualified);
|
|
77
|
+
if (stats.isDirectory()) {
|
|
78
|
+
walkDirSync(qualified, callback);
|
|
79
|
+
}
|
|
80
|
+
else if (stats.isFile()) {
|
|
81
|
+
callback(qualified);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get the path to the Biome executable, if available
|
|
87
|
+
*/
|
|
88
|
+
export async function getBiomePath(dirname) {
|
|
89
|
+
const packageDir = await findUpwards(dirname, async (dir) => {
|
|
90
|
+
const files = await fsp.readdir(dir);
|
|
91
|
+
return files.includes("package.json");
|
|
92
|
+
});
|
|
93
|
+
return Maybe.nullish(packageDir).map((dir) => path.join(dir, "node_modules", ".bin", "biome"));
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Pending debounced calls to generateProjectTypes, keyed by assetsDir */
|
|
2
|
+
const pendingCalls = new Map();
|
|
3
|
+
/**
|
|
4
|
+
* Debounce function calls, grouped by a key.
|
|
5
|
+
*/
|
|
6
|
+
export function debounce(callback, key, debounceMs = 100) {
|
|
7
|
+
const pendingTimeout = pendingCalls.get(key);
|
|
8
|
+
if (pendingTimeout) {
|
|
9
|
+
clearTimeout(pendingTimeout);
|
|
10
|
+
}
|
|
11
|
+
pendingCalls.set(key, setTimeout(() => {
|
|
12
|
+
pendingCalls.delete(key);
|
|
13
|
+
callback();
|
|
14
|
+
}, debounceMs));
|
|
15
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as child_process from "node:child_process";
|
|
2
|
+
import * as fsp from "node:fs/promises";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
export async function rsyncRemoteDirectory(options) {
|
|
5
|
+
const { localDir, remoteDir, host, username, password, port, recursive = true, } = options;
|
|
6
|
+
// Build SSH command with port if specified
|
|
7
|
+
const sshOptions = [];
|
|
8
|
+
if (port) {
|
|
9
|
+
sshOptions.push(`-p ${port}`);
|
|
10
|
+
}
|
|
11
|
+
const sshRsh = sshOptions.length > 0 ? `ssh ${sshOptions.join(" ")}` : "ssh";
|
|
12
|
+
// Construct the rsync command with expanded flags
|
|
13
|
+
const rsyncCommand = [
|
|
14
|
+
"rsync",
|
|
15
|
+
"--archive", // -a: archive mode (recursive, preserves permissions, etc.)
|
|
16
|
+
"--verbose", // -v: verbose mode
|
|
17
|
+
"--compress", // -z: compress data during transfer
|
|
18
|
+
"--progress",
|
|
19
|
+
`--rsh=${sshRsh}`, // Use SSH as the transport with optional port
|
|
20
|
+
`"${localDir}"/`, // Source directory (note the trailing slash!)
|
|
21
|
+
`"${username ? `${username}@` : ""}${host}:${remoteDir}"`, // Destination directory
|
|
22
|
+
];
|
|
23
|
+
// Check if username and password are provided
|
|
24
|
+
if (username && password) {
|
|
25
|
+
// Display warning for password usage
|
|
26
|
+
console.warn(chalk.red("⚠️ WARNING: Using SSH password authentication is not secure!"));
|
|
27
|
+
console.warn(chalk.red(" Consider using SSH keys instead for better security."));
|
|
28
|
+
const portOption = port ? `-p ${port}` : "";
|
|
29
|
+
const sshCommand = `ssh ${username}@${host} ${portOption} -tt 'rsync --archive --verbose --compress --progress "${localDir}/" "${host}:${remoteDir}" && echo "rsync completed successfully"'`;
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
child_process.exec(sshCommand, (error, stdout, stderr) => {
|
|
32
|
+
if (error) {
|
|
33
|
+
console.error("Error executing rsync command:", error);
|
|
34
|
+
reject(error);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
resolve();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const command = rsyncCommand.join(" ");
|
|
45
|
+
child_process.exec(command, (error, stdout, stderr) => {
|
|
46
|
+
if (error) {
|
|
47
|
+
console.error("Error executing rsync command:", error);
|
|
48
|
+
reject(error);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// console.log(stdout);
|
|
52
|
+
resolve();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|