@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.
Files changed (129) hide show
  1. package/LICENSE +9 -0
  2. package/dist/esm/LiqvidDevToolsProvider.js +49 -0
  3. package/dist/esm/api/contract.mjs +48 -0
  4. package/dist/esm/api/project-meta.mjs +33 -0
  5. package/dist/esm/api/recording.mjs +156 -0
  6. package/dist/esm/api/root.mjs +4 -0
  7. package/dist/esm/api/static-file.mjs +82 -0
  8. package/dist/esm/api/types.mjs +1 -0
  9. package/dist/esm/client.mjs +98 -0
  10. package/dist/esm/conventions.mjs +3 -0
  11. package/dist/esm/index.mjs +20 -0
  12. package/dist/esm/initialize.mjs +50 -0
  13. package/dist/esm/jobs/watch-assets.mjs +99 -0
  14. package/dist/esm/jobs/watch-project-files.mjs +216 -0
  15. package/dist/esm/next/api.mjs +48 -0
  16. package/dist/esm/next/page.js +15 -0
  17. package/dist/esm/pages/NewProjectButton.js +62 -0
  18. package/dist/esm/pages/RebuildButton.js +24 -0
  19. package/dist/esm/pages/root-actions.js +151 -0
  20. package/dist/esm/pages/root.js +25 -0
  21. package/dist/esm/pages/root.module.css +326 -0
  22. package/dist/esm/palette.css +279 -0
  23. package/dist/esm/providers/hosting/github-pages.mjs +10 -0
  24. package/dist/esm/providers/hosting/liqvid-studio.mjs +8 -0
  25. package/dist/esm/providers/hosting/s3.mjs +9 -0
  26. package/dist/esm/providers/hosting/sftp.mjs +22 -0
  27. package/dist/esm/providers/index.mjs +10 -0
  28. package/dist/esm/providers/social/bluesky.mjs +8 -0
  29. package/dist/esm/providers/social/facebook.mjs +8 -0
  30. package/dist/esm/providers/social/instagram.mjs +8 -0
  31. package/dist/esm/providers/social/twitter.mjs +7 -0
  32. package/dist/esm/providers/social/youtube.mjs +7 -0
  33. package/dist/esm/providers/types.mjs +1 -0
  34. package/dist/esm/publish.mjs +37 -0
  35. package/dist/esm/recording/RecordingControl.js +110 -0
  36. package/dist/esm/recording/RecordingControl.module.css +0 -0
  37. package/dist/esm/recording/RecordingDialog.js +114 -0
  38. package/dist/esm/recording/RecordingDialog.module.css +194 -0
  39. package/dist/esm/schemas/liqvid-config.mjs +32 -0
  40. package/dist/esm/schemas/project.mjs +27 -0
  41. package/dist/esm/schemas/recording-meta.mjs +11 -0
  42. package/dist/esm/types/assets.mjs +1 -0
  43. package/dist/esm/types.mjs +12 -0
  44. package/dist/esm/ui/Dialog.js +71 -0
  45. package/dist/esm/ui/DockableDialog.js +131 -0
  46. package/dist/esm/ui/DockableDialog.module.css +63 -0
  47. package/dist/esm/ui/RadioTabs.js +13 -0
  48. package/dist/esm/ui/RadioTabs.module.css +54 -0
  49. package/dist/esm/ui/Tabs.js +29 -0
  50. package/dist/esm/ui/Tabs.module.css +31 -0
  51. package/dist/esm/ui/Toast.js +64 -0
  52. package/dist/esm/ui/Toast.module.css +50 -0
  53. package/dist/esm/ui/Toaster.js +13 -0
  54. package/dist/esm/ui/Toaster.module.css +9 -0
  55. package/dist/esm/ui/test.js +14 -0
  56. package/dist/esm/utils/dom.mjs +6 -0
  57. package/dist/esm/utils/fs.mjs +94 -0
  58. package/dist/esm/utils/misc.mjs +15 -0
  59. package/dist/esm/utils/react.mjs +8 -0
  60. package/dist/esm/utils/rsync.mjs +57 -0
  61. package/dist/templates/project.json.hbs +5 -0
  62. package/dist/templates/projects/code/page.tsx.hbs +23 -0
  63. package/dist/templates/projects/code/src/assets.ts.hbs +9 -0
  64. package/dist/templates/projects/code/src/client.tsx.hbs +22 -0
  65. package/dist/templates/projects/code/src/helpers.ts.hbs +21 -0
  66. package/dist/templates/projects/code/src/highlights.ts.hbs +3 -0
  67. package/dist/templates/projects/code/src/markers.ts.hbs +13 -0
  68. package/dist/templates/projects/code/src/project.ts.hbs +23 -0
  69. package/dist/templates/projects/code/template.json +3 -0
  70. package/dist/templates/projects/default/page.tsx.hbs +23 -0
  71. package/dist/templates/projects/default/src/assets.ts.hbs +9 -0
  72. package/dist/templates/projects/default/src/client.tsx.hbs +22 -0
  73. package/dist/templates/projects/default/src/helpers.ts.hbs +21 -0
  74. package/dist/templates/projects/default/src/highlights.ts.hbs +3 -0
  75. package/dist/templates/projects/default/src/markers.ts.hbs +13 -0
  76. package/dist/templates/projects/default/src/project.ts.hbs +23 -0
  77. package/dist/templates/projects/default/template.json +4 -0
  78. package/dist/templates/types.ts.hbs +20 -0
  79. package/dist/types/LiqvidDevToolsProvider.d.ts +12 -0
  80. package/dist/types/api/contract.d.mts +66 -0
  81. package/dist/types/api/project-meta.d.mts +1 -0
  82. package/dist/types/api/recording.d.mts +5 -0
  83. package/dist/types/api/root.d.mts +1 -0
  84. package/dist/types/api/static-file.d.mts +6 -0
  85. package/dist/types/api/types.d.mts +1 -0
  86. package/dist/types/client.d.mts +43 -0
  87. package/dist/types/conventions.d.mts +3 -0
  88. package/dist/types/index.d.mts +19 -0
  89. package/dist/types/initialize.d.mts +14 -0
  90. package/dist/types/jobs/watch-assets.d.mts +18 -0
  91. package/dist/types/jobs/watch-project-files.d.mts +4 -0
  92. package/dist/types/next/api.d.mts +14 -0
  93. package/dist/types/next/page.d.ts +4 -0
  94. package/dist/types/pages/NewProjectButton.d.ts +1 -0
  95. package/dist/types/pages/RebuildButton.d.ts +1 -0
  96. package/dist/types/pages/root-actions.d.ts +29 -0
  97. package/dist/types/pages/root.d.ts +1 -0
  98. package/dist/types/providers/hosting/github-pages.d.mts +11 -0
  99. package/dist/types/providers/hosting/liqvid-studio.d.mts +10 -0
  100. package/dist/types/providers/hosting/s3.d.mts +11 -0
  101. package/dist/types/providers/hosting/sftp.d.mts +13 -0
  102. package/dist/types/providers/index.d.mts +10 -0
  103. package/dist/types/providers/social/bluesky.d.mts +10 -0
  104. package/dist/types/providers/social/facebook.d.mts +10 -0
  105. package/dist/types/providers/social/instagram.d.mts +10 -0
  106. package/dist/types/providers/social/twitter.d.mts +9 -0
  107. package/dist/types/providers/social/youtube.d.mts +9 -0
  108. package/dist/types/providers/types.d.mts +9 -0
  109. package/dist/types/publish.d.mts +1 -0
  110. package/dist/types/recording/RecordingControl.d.ts +16 -0
  111. package/dist/types/recording/RecordingDialog.d.ts +10 -0
  112. package/dist/types/schemas/liqvid-config.d.mts +51 -0
  113. package/dist/types/schemas/project.d.mts +51 -0
  114. package/dist/types/schemas/recording-meta.d.mts +17 -0
  115. package/dist/types/types/assets.d.mts +3 -0
  116. package/dist/types/types.d.mts +20 -0
  117. package/dist/types/ui/Dialog.d.ts +31 -0
  118. package/dist/types/ui/DockableDialog.d.ts +25 -0
  119. package/dist/types/ui/RadioTabs.d.ts +18 -0
  120. package/dist/types/ui/Tabs.d.ts +9 -0
  121. package/dist/types/ui/Toast.d.ts +23 -0
  122. package/dist/types/ui/Toaster.d.ts +6 -0
  123. package/dist/types/ui/test.d.ts +9 -0
  124. package/dist/types/utils/dom.d.mts +3 -0
  125. package/dist/types/utils/fs.d.mts +32 -0
  126. package/dist/types/utils/misc.d.mts +4 -0
  127. package/dist/types/utils/react.d.mts +5 -0
  128. package/dist/types/utils/rsync.d.mts +10 -0
  129. 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,9 @@
1
+ .Toaster {
2
+ bottom: 20px;
3
+ display: flex;
4
+ flex-direction: column;
5
+ position: fixed;
6
+ right: 36px;
7
+ row-gap: 8px;
8
+ z-index: 50;
9
+ }
@@ -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,6 @@
1
+ import { fromZod } from "have-fun/zod";
2
+ export async function fetchJson(Model, input, init) {
3
+ const res = await fetch(input, init);
4
+ const json = await res.json();
5
+ return fromZod(Model.safeParse(json));
6
+ }
@@ -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,8 @@
1
+ import { useCallback, useState } from "react";
2
+ export function useToggle(defaultValue) {
3
+ const [value, set] = useState(!!defaultValue);
4
+ const toggle = useCallback(() => {
5
+ set((x) => !x);
6
+ }, []);
7
+ return { set, toggle, value };
8
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "aspectRatio": "16:9",
3
+ "description": "",
4
+ "name": "{{name}}"
5
+ }