fubi 0.4.8 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.prettierignore +9 -0
  2. package/.prettierrc +23 -0
  3. package/.vscode/extensions.json +3 -0
  4. package/config.toml +3 -0
  5. package/demo/index.html +64 -0
  6. package/index.html +19 -0
  7. package/package.json +44 -39
  8. package/publish.js +77 -0
  9. package/src/components/App.svelte +17 -0
  10. package/src/components/Debug.svelte +39 -0
  11. package/src/components/Layout.svelte +47 -0
  12. package/src/components/comment/CommentCard.svelte +49 -0
  13. package/src/components/comment/CommentFocusElement.svelte +14 -0
  14. package/src/components/comment/CommentMenu.svelte +18 -0
  15. package/src/components/comment/CommentPriority.svelte +31 -0
  16. package/src/components/comment/CommentReplyButton.svelte +35 -0
  17. package/src/components/comment/CommentUser.svelte +22 -0
  18. package/src/components/icons/AlertIcon.svelte +7 -0
  19. package/src/components/icons/CancelIcon.svelte +8 -0
  20. package/src/components/icons/CheckCircle.svelte +7 -0
  21. package/src/components/icons/CheckIcon.svelte +7 -0
  22. package/src/components/icons/CheckSmallIcon.svelte +7 -0
  23. package/src/components/icons/ChevronLeftIcon.svelte +7 -0
  24. package/src/components/icons/ChevronRightIcon.svelte +7 -0
  25. package/src/components/icons/ChevronsUpIcon.svelte +8 -0
  26. package/src/components/icons/DotsIcon.svelte +9 -0
  27. package/src/components/icons/PlusIcon.svelte +8 -0
  28. package/src/components/index.ts +0 -0
  29. package/src/components/toolbar/ButtonToolbar.svelte +29 -0
  30. package/src/components/toolbar/Menu.svelte +42 -0
  31. package/src/components/toolbar/Toolbar.svelte +55 -0
  32. package/src/components/ui/Button.svelte +33 -0
  33. package/src/components/ui/ButtonIcon.svelte +31 -0
  34. package/src/components/ui/ButtonSmall.svelte +12 -0
  35. package/src/components/ui/ButtonTabbar.svelte +35 -0
  36. package/src/components/ui/Chip.svelte +28 -0
  37. package/src/components/ui/Icon.svelte +38 -0
  38. package/src/components/ui/Input.svelte +9 -0
  39. package/src/components/ui/List.svelte +18 -0
  40. package/src/components/ui/ListItem.svelte +87 -0
  41. package/src/components/ui/ListLoader.svelte +20 -0
  42. package/src/components/ui/Loader.svelte +39 -0
  43. package/src/components/ui/Logo.svelte +20 -0
  44. package/src/components/ui/Msg.svelte +21 -0
  45. package/src/components/ui/Navbar.svelte +33 -0
  46. package/src/components/ui/Page.svelte +67 -0
  47. package/src/components/ui/Tabbar.svelte +60 -0
  48. package/src/components/ui/Toggle.svelte +8 -0
  49. package/src/components/ui/Window.svelte +61 -0
  50. package/src/index.ts +28 -0
  51. package/src/lib/actions.ts +58 -0
  52. package/src/lib/haptics.ts +94 -0
  53. package/src/lib/logger.ts +22 -0
  54. package/src/lib/routes.ts +37 -0
  55. package/src/lib/utils.ts +27 -0
  56. package/src/modules/app.svelte.ts +66 -0
  57. package/src/modules/auth.svelte.ts +80 -0
  58. package/src/modules/collection.svelte.ts +79 -0
  59. package/src/modules/comments.svelte.ts +34 -0
  60. package/src/modules/dom.svelte.ts +26 -0
  61. package/src/modules/domains.svelte.ts +91 -0
  62. package/src/modules/environment.svelte.ts +10 -0
  63. package/src/modules/events.svelte.ts +23 -0
  64. package/src/modules/folders.svelte.ts +64 -0
  65. package/src/modules/home.svelte.ts +29 -0
  66. package/src/modules/hover.svelte.ts +3 -0
  67. package/src/modules/index.ts +28 -0
  68. package/src/modules/keys.svelte.ts +72 -0
  69. package/src/modules/module.svelte.ts +47 -0
  70. package/src/modules/navbar.svelte.ts +11 -0
  71. package/src/modules/pages.svelte.ts +126 -0
  72. package/src/modules/project.svelte.ts +70 -0
  73. package/src/modules/router.svelte.ts +99 -0
  74. package/src/modules/tabbar.svelte.ts +21 -0
  75. package/src/modules/teams.svelte.ts +0 -0
  76. package/src/modules/toolbar.svelte.ts +34 -0
  77. package/src/modules/watch.svelte.ts +8 -0
  78. package/src/modules/win.svelte.ts +273 -0
  79. package/src/pages/Comments.svelte +32 -0
  80. package/src/pages/Error.svelte +25 -0
  81. package/src/pages/Folders.svelte +53 -0
  82. package/src/pages/Login.svelte +44 -0
  83. package/src/pages/Pages.svelte +75 -0
  84. package/src/pages/Thread.svelte +11 -0
  85. package/src/styles/global.css +16 -0
  86. package/src/styles/router.css +79 -0
  87. package/src/styles/styles.css +159 -0
  88. package/src/styles/tailwind.css +115 -0
  89. package/src/styles/variables.css +99 -0
  90. package/src/test.ts +22 -0
  91. package/src/types/fubi.ts +0 -0
  92. package/src/types/msg.ts +5 -0
  93. package/src/types/pocketbase.ts +7 -0
  94. package/src/types/user.ts +8 -0
  95. package/svelte.config.js +11 -0
  96. package/tsconfig.json +41 -0
  97. package/vite.config.ts +40 -0
  98. package/dist/fonts/inter-latin-400-normal.woff2 +0 -0
  99. package/dist/fonts/inter-latin-500-normal.woff2 +0 -0
  100. package/dist/fonts/inter-latin-600-normal.woff2 +0 -0
  101. package/dist/fonts/inter-latin-700-normal.woff2 +0 -0
  102. package/dist/fubi.iife.js +0 -108
  103. package/dist/fubi.js +0 -9802
@@ -0,0 +1,99 @@
1
+ import { mount } from "svelte";
2
+ import { routes } from "../lib/routes";
3
+ import { logger } from "../lib/logger";
4
+ import { Module } from "./module.svelte";
5
+
6
+ import { UserType } from "../types/user";
7
+
8
+ const NAVIGATION_DURATION = 400;
9
+
10
+ const log = logger("router");
11
+
12
+ export class Router extends Module {
13
+ routes = $state(routes);
14
+ navigating = $state(false);
15
+ direction = $state<"forward" | "back">("forward");
16
+
17
+ previous = $state(null as string | null);
18
+ declare current: string;
19
+ next = $state(null as string | null);
20
+
21
+ constructor() {
22
+ super();
23
+ this.storage<string>("current", "fubi-router-current", "login");
24
+ }
25
+
26
+ start() {
27
+ $effect(() => {
28
+ if (this.modules.win.el) this.mountPages();
29
+ });
30
+
31
+ console.log(routes);
32
+ }
33
+
34
+ mountPages() {
35
+ Object.entries(this.routes).forEach(([name, route]) => {
36
+ log.info("Mounting page:", name);
37
+ if (!this.modules.win.el) return;
38
+ mount(route.component, { target: this.modules.win.el });
39
+ });
40
+ }
41
+
42
+ determineRoute(user: UserType | null, isAllowed: boolean) {
43
+ if (!user) return ["login", "back"];
44
+ if (!isAllowed) return ["login", "back"];
45
+
46
+ if (this.current === "login") {
47
+ const accessibleFolders = this.modules.folders.data.filter((f) => f.unlocked);
48
+ if (accessibleFolders.length === 1) return ["home", "forward"];
49
+ else return ["folders", "forward"];
50
+ }
51
+
52
+ if (this.current === "pages") return ["comments"];
53
+
54
+ return [this.current || "home"];
55
+ }
56
+
57
+ hasRoute(name: string) {
58
+ return name in this.routes;
59
+ }
60
+
61
+ goto(name: string, direction: "forward" | "back" = "forward") {
62
+ if (!this.hasRoute(name) || this.current === name) {
63
+ log.error("Invalid or same route, cannot navigate to:", name);
64
+ return;
65
+ }
66
+ if (!this.modules.auth.user) {
67
+ log.warn("No users module, cannot navigate.");
68
+ this.transition("login", "back");
69
+ return;
70
+ }
71
+
72
+ const nextRoute = this.modules.auth.user ? name : "login";
73
+ this.transition(nextRoute, direction);
74
+ }
75
+
76
+ private transition(pageName: string, direction: "forward" | "back") {
77
+ if (this.navigating) {
78
+ log.warn("Already navigating, ignoring.");
79
+ return;
80
+ }
81
+
82
+ this.navigating = true;
83
+ this.direction = direction;
84
+
85
+ if (direction === "forward") this.next = pageName;
86
+ else this.previous = pageName;
87
+
88
+ this.modules.win.el?.classList.add(`router-transition-${direction}`);
89
+
90
+ setTimeout(() => {
91
+ this.navigating = false;
92
+ this.previous = this.current;
93
+ this.current = pageName;
94
+ this.next = null;
95
+ this.modules.win.el?.classList.remove(`router-transition-${direction}`);
96
+ this.modules.win.focusPageContent();
97
+ }, NAVIGATION_DURATION);
98
+ }
99
+ }
@@ -0,0 +1,21 @@
1
+ import { Module } from "./module.svelte";
2
+
3
+ export class Tabbar extends Module {
4
+ visible = $state(false);
5
+
6
+ tabActive = $derived(
7
+ (this.modules.router.navigating
8
+ ? this.modules.router.direction === "forward"
9
+ ? this.modules.router.next
10
+ : this.modules.router.previous
11
+ : this.modules.router.current) === "comments"
12
+ ? "comments"
13
+ : (this.modules.router.navigating
14
+ ? this.modules.router.direction === "forward"
15
+ ? this.modules.router.next
16
+ : this.modules.router.previous
17
+ : this.modules.router.current) === "folders"
18
+ ? "folders"
19
+ : "pages",
20
+ );
21
+ }
File without changes
@@ -0,0 +1,34 @@
1
+ import type { App } from "./app.svelte";
2
+ import { hasFocus } from "@utils";
3
+ import { Module } from "./module.svelte";
4
+
5
+ export class Toolbar extends Module {
6
+ wrapper!: HTMLElement;
7
+ hovering = $state(false);
8
+ focusWithin = $state(false);
9
+
10
+ visible = $state(false);
11
+ minimized = $state(true);
12
+
13
+ logo = $state(true);
14
+
15
+ constructor(wrapper: HTMLElement) {
16
+ super();
17
+ this.wrapper = wrapper;
18
+ }
19
+
20
+ start() {
21
+ this.modules.keys.on("c", () => {
22
+ if (hasFocus()) return;
23
+ console.log("you pressed c, should enable comment mode");
24
+ });
25
+ }
26
+
27
+ handlePointerEnter() {
28
+ this.hovering = true;
29
+ }
30
+
31
+ handlePointerLeave() {
32
+ this.hovering = false;
33
+ }
34
+ }
@@ -0,0 +1,8 @@
1
+ import { untrack } from "svelte";
2
+
3
+ export function watch(fn: () => void, deps: () => any[]) {
4
+ $effect(() => {
5
+ deps();
6
+ untrack(fn);
7
+ });
8
+ }
@@ -0,0 +1,273 @@
1
+ // modules
2
+ import { Spring } from "svelte/motion";
3
+
4
+ import { hasFocus } from "@utils";
5
+ import { Module } from "./module.svelte.js";
6
+ import { haptics } from "@lib/haptics.js";
7
+
8
+ const MIDDLE_TO_TOP_TRESHOLD = 50; // px distance from middle to top to trigger open
9
+ const MIDDLE_TO_BOTTOM_TRESHOLD = 250; // px distance from middle to bottom to trigger close
10
+
11
+ const BREAKPOINT_TRESHOLD = 30;
12
+
13
+ export class Win extends Module {
14
+ el = $state(null as HTMLDivElement | null);
15
+
16
+ declare opened: boolean;
17
+
18
+ contentScroll = $state<number>(0);
19
+
20
+ height = new Spring(0, { stiffness: 0.2, damping: 0.4 });
21
+
22
+ breakpoints = $state({
23
+ bottom: 0,
24
+ middle: this.calculateBreakpoint(),
25
+ // get client height
26
+ top: this.calculateBreakpoint(0.99),
27
+ });
28
+ currentBreakpoint = $state<number>(this.breakpoints.middle);
29
+ nextBreakpoint = $state<number>(this.breakpoints.middle);
30
+
31
+ dragging = $state({
32
+ active: false,
33
+ start: 0,
34
+ current: 0,
35
+ difference: 0,
36
+ startHeight: 0,
37
+ height: 0,
38
+ direction: null as "up" | "down" | null,
39
+ hasMoved: false,
40
+ });
41
+
42
+ constructor() {
43
+ super();
44
+ this.storage<boolean>("opened", "fubi-win-opened", true);
45
+ }
46
+
47
+ start() {
48
+ this.modules.keys.on("m", () => {
49
+ if (!hasFocus()) this.toggle();
50
+ });
51
+
52
+ this.modules.keys.on("Escape", () => {
53
+ if (!hasFocus()) this.close();
54
+ });
55
+
56
+ // check if the window is opened
57
+ if (this.opened) {
58
+ this.modules.dom.scroll.disable({ touchOnly: true });
59
+ }
60
+
61
+ // Setup touch handling
62
+ this.setupTouchHandling();
63
+ }
64
+
65
+ setupTouchHandling() {
66
+ // Wait for element
67
+ if (!this.el) {
68
+ requestAnimationFrame(() => this.setupTouchHandling());
69
+ return;
70
+ }
71
+
72
+ let touchStartY = 0;
73
+ let startScrollTop = 0;
74
+ let deciding = true;
75
+
76
+ // Use native touch events for better control
77
+ this.el.addEventListener(
78
+ "touchstart",
79
+ (e) => {
80
+ if (!this.opened) return;
81
+
82
+ touchStartY = e.touches[0].clientY;
83
+ startScrollTop = this.contentScroll;
84
+ deciding = true;
85
+ },
86
+ { passive: true },
87
+ );
88
+
89
+ this.el.addEventListener(
90
+ "touchmove",
91
+ (e) => {
92
+ if (!this.opened || !deciding) return;
93
+
94
+ const touchY = e.touches[0].clientY;
95
+ const diff = touchY - touchStartY;
96
+
97
+ // After 5px of movement, decide what to do
98
+ if (Math.abs(diff) > 5) {
99
+ const scrollable = this.el?.querySelector(
100
+ ".page-content, .content, [data-scrollable]",
101
+ ) as HTMLElement;
102
+ const isAtBottom = scrollable
103
+ ? this.contentScroll >= scrollable.scrollHeight - scrollable.clientHeight - 1
104
+ : false;
105
+
106
+ const shouldPreventScroll =
107
+ (this.contentScroll <= 0 && diff > 0) || // At top, dragging down
108
+ (isAtBottom && diff < 0 && this.currentBreakpoint !== this.breakpoints.top); // At bottom, dragging up
109
+
110
+ if (shouldPreventScroll) {
111
+ e.preventDefault(); // This will prevent the scroll
112
+ deciding = false; // Decision made
113
+ } else {
114
+ deciding = false; // Let it scroll
115
+ }
116
+ }
117
+ },
118
+ { passive: false },
119
+ );
120
+ }
121
+
122
+ open() {
123
+ this.currentBreakpoint = this.breakpoints.middle;
124
+ this.opened = true;
125
+ this.modules.dom.scroll.disable({ touchOnly: true });
126
+
127
+ // focus element
128
+ this.focusPageContent();
129
+ }
130
+ close() {
131
+ this.currentBreakpoint = this.breakpoints.bottom;
132
+ this.opened = false;
133
+ this.modules.dom.scroll.enable();
134
+ }
135
+ toggle() {
136
+ this[this.opened ? "close" : "open"]();
137
+ }
138
+
139
+ getCurrentHeight(max: boolean = false): number {
140
+ if (!this.el) return 0;
141
+
142
+ const style = getComputedStyle(this.el);
143
+ const height = style[max ? "maxHeight" : "height"];
144
+
145
+ if (!height || height === "none") return 0;
146
+
147
+ return parseFloat(height) || 0;
148
+ }
149
+
150
+ calculateBreakpoint(fragment: number = 0.55): number {
151
+ const deviceHeight = window.innerHeight;
152
+ const calculated = deviceHeight * fragment;
153
+ return Math.max(200, Math.min(1000, calculated));
154
+ }
155
+
156
+ focusPageContent() {
157
+ const currentPageName = this.modules.router.current;
158
+ const page = this.el?.querySelectorAll(`[data-fubi-page="${currentPageName}"]`);
159
+ const pageContent = page?.[0]?.querySelector(".page-content") as HTMLDivElement | null;
160
+
161
+ if (pageContent) pageContent.focus();
162
+ }
163
+
164
+ drag = {
165
+ start: (e: PointerEvent) => {
166
+ if (!this.modules.environment.touch) return;
167
+
168
+ // DON'T stop propagation here - we don't know if it's a drag yet!
169
+ // Check if the target is an interactive element
170
+ const target = e.target as HTMLElement;
171
+ if (target.closest("button, a, input, label, textarea, select")) {
172
+ return; // Don't start drag on interactive elements
173
+ }
174
+
175
+ this.dragging.hasMoved = false;
176
+ this.dragging.active = true;
177
+ this.dragging.start = e.y;
178
+ this.dragging.startHeight = this.getCurrentHeight();
179
+ this.dragging.height = this.dragging.startHeight;
180
+ },
181
+
182
+ move: (e: PointerEvent) => {
183
+ if (!this.dragging.active || !this.modules.environment.touch) return;
184
+
185
+ this.dragging.current = e.y;
186
+ this.dragging.difference = this.dragging.current - this.dragging.start;
187
+
188
+ // Only process if we're actually dragging (not scrolling)
189
+ // The touch handler above should have prevented scroll if needed
190
+ if (Math.abs(this.dragging.difference) > 2) {
191
+ this.dragging.hasMoved = true;
192
+
193
+ const isAtTop = this.contentScroll <= 0;
194
+ const isDraggingDown = this.dragging.difference > 0;
195
+
196
+ // Only actually drag if we're in a drag scenario
197
+ // (at top dragging down, or at bottom dragging up)
198
+ if (!(isAtTop && isDraggingDown)) {
199
+ // Check if at bottom
200
+ const scrollable = this.el?.querySelector(
201
+ ".page-content, .content, [data-scrollable]",
202
+ ) as HTMLElement;
203
+ const isAtBottom = scrollable
204
+ ? this.contentScroll >= scrollable.scrollHeight - scrollable.clientHeight - 1
205
+ : false;
206
+ const isDraggingUp = this.dragging.difference < 0;
207
+
208
+ if (!(isAtBottom && isDraggingUp && this.currentBreakpoint !== this.breakpoints.top)) {
209
+ // We shouldn't be dragging, bail out
210
+ this.dragging.active = false;
211
+ return;
212
+ }
213
+ }
214
+ }
215
+
216
+ if (this.currentBreakpoint === this.breakpoints.middle) {
217
+ if (
218
+ this.dragging.direction === "up" &&
219
+ this.dragging.height > this.breakpoints.middle + BREAKPOINT_TRESHOLD
220
+ ) {
221
+ this.nextBreakpoint = this.breakpoints.top;
222
+ } else if (
223
+ this.dragging.direction === "down" &&
224
+ this.dragging.height < this.breakpoints.middle - BREAKPOINT_TRESHOLD
225
+ ) {
226
+ this.nextBreakpoint = this.breakpoints.bottom;
227
+ } else {
228
+ this.nextBreakpoint = this.breakpoints.middle;
229
+ }
230
+ }
231
+
232
+ if (this.currentBreakpoint === this.breakpoints.top) {
233
+ if (
234
+ this.dragging.direction === "down" &&
235
+ this.dragging.height < this.breakpoints.top - MIDDLE_TO_TOP_TRESHOLD
236
+ ) {
237
+ this.nextBreakpoint = this.breakpoints.middle;
238
+ } else {
239
+ this.nextBreakpoint = this.breakpoints.top;
240
+ }
241
+ }
242
+
243
+ this.dragging.height = this.dragging.startHeight - this.dragging.difference;
244
+ this.dragging.direction = this.dragging.difference < 0 ? "up" : "down";
245
+ },
246
+
247
+ end: (e: PointerEvent) => {
248
+ // Always prevent event propagation if we were in drag mode
249
+ // This stops the click from reaching the backdrop button
250
+ if (this.dragging.active || this.dragging.hasMoved) {
251
+ e.preventDefault();
252
+ e.stopPropagation();
253
+ e.stopImmediatePropagation();
254
+ }
255
+
256
+ if (!this.dragging.active) return;
257
+
258
+ if (this.dragging.hasMoved) {
259
+ this.currentBreakpoint = this.nextBreakpoint;
260
+ if (this.currentBreakpoint === this.breakpoints.bottom) this.close();
261
+ }
262
+
263
+ // Reset all drag state
264
+ this.dragging.active = false;
265
+ this.dragging.current = 0;
266
+ this.dragging.startHeight = 0;
267
+ this.dragging.height = 0;
268
+ this.dragging.difference = 0;
269
+ this.dragging.direction = null;
270
+ this.dragging.hasMoved = false;
271
+ },
272
+ };
273
+ }
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import { getModules } from "@modules";
3
+
4
+ // components
5
+ import Page from "@ui/Page.svelte";
6
+
7
+ import Navbar from "@components/ui/Navbar.svelte";
8
+ import CommentCard from "@components/comment/CommentCard.svelte";
9
+
10
+ // icons
11
+ import { SquareLock02Icon } from "@hugeicons-pro/core-solid-rounded";
12
+ import ListLoader from "@components/ui/ListLoader.svelte";
13
+
14
+ const { comments, pages } = getModules();
15
+ </script>
16
+
17
+
18
+ <Page name="comments" title={pages?.current?.name}>
19
+ <div class="flex flex-col gap-8">
20
+ {#each comments?.data ?? [] as comment, index }
21
+ <CommentCard {...comment} />
22
+ {:else}
23
+ {#if comments?.loading}
24
+ <ListLoader spinner />
25
+ {:else if comments?.error}
26
+ <p>Error loading</p>
27
+ {:else if comments?.empty}
28
+ <p class="p-4 text-center text-sm text-gray-500">No comments</p>
29
+ {/if}
30
+ {/each}
31
+ </div>
32
+ </Page>
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import { getModules } from "@modules";
3
+
4
+ // components
5
+ import { HugeiconsIcon } from "@hugeicons/svelte";
6
+
7
+ import Page from "@ui/Page.svelte";
8
+ import ButtonSmall from "@components/ui/ButtonSmall.svelte";
9
+
10
+ // icons
11
+ import { AlertDiamondIcon } from "@hugeicons-pro/core-solid-rounded";
12
+ import Button from "@components/ui/Button.svelte";
13
+
14
+ </script>
15
+ <Page name="error" title="Error">
16
+ <div class="h-full flex-center flex-col gap-16">
17
+ <div class="flex-center flex-col gap-8 text-center bg-red-500/20 glass w-full p-16 rounded-[16px]">
18
+ <HugeiconsIcon icon={AlertDiamondIcon} className="size-32 text-red-400" />
19
+ <p class="text-sm text-red-200">An unexpected error has occurred. Please try again later.</p>
20
+ <div class="flex-center w-full mt-4">
21
+ <ButtonSmall onclick={() => window.location.reload()}>Refresh</ButtonSmall>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </Page>
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import { getModules } from "@modules";
3
+ const { router, folders } = getModules();
4
+
5
+ import type { FolderType } from "@modules/folders.svelte";
6
+
7
+ // components
8
+ import Page from "@ui/Page.svelte";
9
+ import List from "@ui/List.svelte";
10
+ import ListItem from "@ui/ListItem.svelte";
11
+ import ListLoader from "@ui/ListLoader.svelte";
12
+ import Chip from "@ui/Chip.svelte";
13
+
14
+ // icons
15
+ import {
16
+ Folder01Icon,
17
+ FolderLockedIcon,
18
+ User02Icon
19
+ } from "@hugeicons-pro/core-solid-rounded";
20
+
21
+ const handleFolderSelection = (folder: FolderType) => {
22
+ folders.select(folder);
23
+ router.goto("pages");
24
+ }
25
+ </script>
26
+
27
+ <Page name="folders" title="Folders">
28
+ <List>
29
+ {#each folders?.data ?? [] as folder, index }
30
+ <ListItem
31
+ title={folder.name}
32
+ subtitle={folder.locked && "You don't have access"}
33
+ icon={folder.locked ? FolderLockedIcon : Folder01Icon}
34
+ disabled={folder.locked ?? null}
35
+ {index}
36
+ active={folders.current?.id === folder.id}
37
+ onclick={() => folder ? handleFolderSelection(folder) : null}
38
+ >
39
+ {#snippet after()}
40
+ {#if !folder.locked}<Chip theme="sky" icon={User02Icon}>{folder.users.length}</Chip>{/if}
41
+ {/snippet}
42
+ </ListItem>
43
+ {:else}
44
+ {#if folders?.loading}
45
+ <ListLoader spinner />
46
+ {:else if folders?.error}
47
+ <p>Error loading</p>
48
+ {:else if folders?.empty}
49
+ <p class="p-4 text-center text-sm text-gray-500">No folders</p>
50
+ {/if}
51
+ {/each}
52
+ </List>
53
+ </Page>
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import { getModules } from "@modules";
3
+
4
+ // components
5
+ import Page from "@ui/Page.svelte";
6
+ import List from "@ui/List.svelte";
7
+ import Input from "@ui/Input.svelte";
8
+ import Button from "@ui/Button.svelte";
9
+ import Msg from "@ui/Msg.svelte";
10
+
11
+
12
+
13
+ // icons
14
+ import { SquareLock02Icon } from "@hugeicons-pro/core-solid-rounded";
15
+ import { HugeiconsIcon } from "@hugeicons/svelte";
16
+ import Logo from "@components/ui/Logo.svelte";
17
+
18
+ const { auth, router } = getModules();
19
+
20
+ let email = $state("");
21
+ let password = $state("");
22
+
23
+ const handleLogin = (e: Event) => {
24
+ e.preventDefault();
25
+ auth.login(email, password);
26
+ // password = "";
27
+ }
28
+ </script>
29
+
30
+
31
+ <Page name="login" navbar={false} tabbar={false}>
32
+ <form class="flex flex-col gap-8 px-8 w-full" onsubmit={(e) => handleLogin(e)}>
33
+ <div class="flex-center flex-col pt-16">
34
+ <HugeiconsIcon icon={SquareLock02Icon} className="size-32 text-sky-500" />
35
+ <p class="text-[18px]/none">Sign In to fubi</p>
36
+ </div>
37
+ <List>
38
+ <Input name="email" type="email" label="E-mail" autocomplete="email" required bind:value={email} />
39
+ <Input name="password" type="password" label="Password" autocomplete="password" required bind:value={password} />
40
+ </List>
41
+ <Msg msg={auth.msg} />
42
+ <Button loading={auth.loading} disabled={auth.loading || router.navigating}>Login</Button>
43
+ </form>
44
+ </Page>
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import { getModules } from "@modules";
3
+ const { win, folders, router, domains, pages } = getModules();
4
+
5
+ // components
6
+ import List from "@ui/List.svelte";
7
+ import ListItem from "@ui/ListItem.svelte";
8
+ import ListLoader from "@ui/ListLoader.svelte";
9
+ import Page from "@ui/Page.svelte";
10
+ import Chip from "@ui/Chip.svelte";
11
+ import Navbar from "@ui/Navbar.svelte";
12
+ import Loader from "@ui/Loader.svelte";
13
+
14
+ import { HugeiconsIcon } from "@hugeicons/svelte";
15
+
16
+ // icons
17
+ import {
18
+ FileEmpty02Icon,
19
+ File02Icon,
20
+ AiFileIcon
21
+ } from "@hugeicons-pro/core-solid-rounded";
22
+ import Icon from "@components/ui/Icon.svelte";
23
+ </script>
24
+
25
+ <Page name="pages" title={folders.current.name}>
26
+ <List>
27
+ {#if pages.isNew}
28
+ <ListItem
29
+ classIcon="text-white"
30
+ title={pages.current?.name}
31
+ subtitle="No Comments"
32
+ active
33
+ onclick={() => router.goto("comments")}
34
+ >
35
+ {#snippet start()}
36
+ <Loader progress={0} />
37
+ {/snippet}
38
+ {#snippet after()}
39
+ <Chip theme="sky">Current</Chip>
40
+ {/snippet}
41
+ </ListItem>
42
+ {/if}
43
+ {#each pages?.data as { comments, totalComments, doneComments, progress, pathname, name }, index}
44
+ <a href={pathname === domains?.url?.pathname ? "#" : pathname}>
45
+ <ListItem
46
+ class={comments.length > 0 ? "text-white" : pathname === domains?.url?.pathname ? "text-white" : "text-neutral-400"}
47
+ classIcon={pathname === domains?.url?.pathname ? "text-slate-500" : ""}
48
+ title={name}
49
+ subtitle={comments.length > 0 ? `${doneComments}/${totalComments} Done` : "No Comments"}
50
+ active={pathname === domains?.url?.pathname}
51
+ {index}
52
+ onclick={pathname === domains?.url?.pathname ? () => router.goto("comments") : null}
53
+ >
54
+ {#snippet start()}
55
+ {#if progress === 100}
56
+ <Icon name="check-circle" class="size-22 text-sky-500" />
57
+ {:else}
58
+ <Loader progress={progress} />
59
+ {/if}
60
+ {/snippet}
61
+
62
+ {#snippet after()}
63
+ {#if pathname === domains?.url?.pathname}<Chip theme="sky">Current</Chip>{/if}
64
+ {/snippet}
65
+ </ListItem>
66
+ </a>
67
+ {:else}
68
+ {#if pages?.loading}
69
+ <ListLoader spinner />
70
+ {:else if pages?.error}
71
+ <p>Error loading</p>
72
+ {/if}
73
+ {/each}
74
+ </List>
75
+ </Page>
@@ -0,0 +1,11 @@
1
+ <script lang="ts">
2
+ import { getModules } from "@modules";
3
+
4
+ // components
5
+ import Page from "@ui/Page.svelte";
6
+ </script>
7
+
8
+
9
+ <Page name="thread" tabbar={false} navbar={false}>
10
+ <p>Thread</p>
11
+ </Page>