orcas-angular 1.0.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 (42) hide show
  1. package/LICENSE +159 -0
  2. package/README.md +17 -0
  3. package/async/README.md +46 -0
  4. package/async/async.ts +16 -0
  5. package/async/cancellation-token.ts +90 -0
  6. package/dev/README.md +41 -0
  7. package/dev/console-hook.ts +25 -0
  8. package/dev/debug.service.ts.example +29 -0
  9. package/framework/README.md +34 -0
  10. package/framework/services-init.ts +25 -0
  11. package/index.ts +21 -0
  12. package/localization/README.md +73 -0
  13. package/localization/localization.interface.ts +18 -0
  14. package/localization/localization.service.ts +131 -0
  15. package/localization/localize.pipe.ts +30 -0
  16. package/log/README.md +275 -0
  17. package/log/echo-provider.ts +27 -0
  18. package/log/echo.ts +635 -0
  19. package/log/index.ts +6 -0
  20. package/log/log-systems.ts +20 -0
  21. package/navigation/README.md +47 -0
  22. package/navigation/back-on-click.directive.ts +19 -0
  23. package/navigation/index.ts +3 -0
  24. package/navigation/navigation-stack.service.ts +33 -0
  25. package/package.json +29 -0
  26. package/storage/README.md +75 -0
  27. package/storage/capacitor-files.service.ts +38 -0
  28. package/storage/file-box.service.ts +112 -0
  29. package/storage/files.ts +42 -0
  30. package/storage/key-signals.ts +49 -0
  31. package/storage/local-storage-files.service.ts +49 -0
  32. package/storage/settings-signals.service.ts +24 -0
  33. package/storage/settings.service.ts +24 -0
  34. package/storage/tauri-files.service.ts +69 -0
  35. package/theme/README.md +44 -0
  36. package/theme/theme.service.ts +33 -0
  37. package/ui/README.md +42 -0
  38. package/ui/context-menu/context-button.component.ts +55 -0
  39. package/ui/context-menu/context-header.component.ts +15 -0
  40. package/ui/context-menu/context-menu-trigger.directive.ts +26 -0
  41. package/ui/context-menu/context-menu.component.ts +95 -0
  42. package/ui/context-menu/index.ts +4 -0
@@ -0,0 +1,33 @@
1
+ import { computed, effect, inject, Injectable } from '@angular/core';
2
+ import { SettingsService } from "../storage/settings.service";
3
+
4
+ export enum ThemeType {
5
+ Unset = '',
6
+ Light = 'light',
7
+ Dark = 'dark',
8
+ }
9
+
10
+ @Injectable({
11
+ providedIn: 'root'
12
+ })
13
+ export class ThemeService {
14
+ private settings = inject(SettingsService);
15
+ public $theme = this.settings.getNewSignal<ThemeType>(ThemeType.Unset, 'theme');
16
+
17
+ public $darkMode = computed(() => {
18
+ const theme = this.$theme();
19
+ let isDarkMode: boolean = theme === ThemeType.Unset
20
+ ? window.matchMedia('(prefers-color-scheme: dark)').matches
21
+ : theme === ThemeType.Dark;
22
+
23
+ return isDarkMode;
24
+ });
25
+
26
+ private effectSetDarkMode = effect(async () => {
27
+ document.documentElement.classList.toggle('dark', this.$darkMode());
28
+ });
29
+
30
+ public async setTheme(theme: ThemeType) {
31
+ await this.settings.set(theme, 'theme');
32
+ }
33
+ }
package/ui/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # ui
2
+
3
+ Reusable UI components for the application. Currently contains the context menu system.
4
+
5
+ ## Folders
6
+
7
+ ### `context-menu/`
8
+
9
+ A fully self-contained context menu implementation built with Angular standalone components and directives. It handles viewport clamping automatically so that menus never render off-screen, and supports nested sub-menus.
10
+
11
+ **Components and directives:**
12
+
13
+ | File | Selector | Description |
14
+ |---|---|---|
15
+ | `context-menu.component.ts` | `<context-menu>` | Container that renders a floating menu panel at a given (x, y) coordinate. Can be used as a top-level menu or as an `isSubmenu` variant that positions itself adjacent to the parent button. |
16
+ | `context-button.component.ts` | `<context-button>` | Menu item button. Supports `danger` and `disabled` inputs and an optional `hasSubmenu` flag that reveals a `▶` indicator and conditionally renders a nested `<context-menu>` on hover. |
17
+ | `context-header.component.ts` | `<context-header>` | Non-interactive section header / label for grouping menu items. |
18
+ | `context-menu-trigger.directive.ts` | `[appContextMenu]` | Applied to any element. Intercepts the `contextmenu` event, prevents the default browser menu, and calls `show(x, y)` on the bound `ContextMenuComponent`. Emits a `beforeOpen` output before the menu opens, which is useful for preparing dynamic menu content. |
19
+ | `index.ts` | — | Public barrel re-exporting all of the above. |
20
+
21
+ **Usage:**
22
+
23
+ ```html
24
+ <div [appContextMenu]="menu" (beforeOpen)="prepareMenu()">
25
+ Right-click me
26
+ </div>
27
+
28
+ <context-menu #menu>
29
+ <context-header>Actions</context-header>
30
+
31
+ <context-button (click)="doSomething()">Do something</context-button>
32
+
33
+ <context-button danger (click)="deleteItem()">Delete</context-button>
34
+
35
+ <context-button [hasSubmenu]="true">
36
+ More…
37
+ <context-menu [isSubmenu]="true">
38
+ <context-button (click)="doNested()">Nested action</context-button>
39
+ </context-menu>
40
+ </context-button>
41
+ </context-menu>
42
+ ```
@@ -0,0 +1,55 @@
1
+ import { Component, input, signal, booleanAttribute } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ @Component({
5
+ selector: 'context-button',
6
+ standalone: true,
7
+ imports: [CommonModule],
8
+ template: `
9
+ <div class="relative" (mouseenter)="onMouseEnter()" (mouseleave)="onMouseLeave()">
10
+ <button
11
+ class="w-full text-left px-3 py-2 text-sm transition-colors duration-100 flex items-center justify-between gap-2"
12
+ [class.hover:bg-light-bg-secondary]="!disabled()"
13
+ [class.dark:hover:bg-[#383838]]="!disabled()"
14
+ [class.text-red-500]="danger() && !disabled()"
15
+ [class.hover:bg-red-500]="danger() && !disabled()"
16
+ [class.hover:text-white]="danger() && !disabled()"
17
+ [class.opacity-50]="disabled()"
18
+ [class.cursor-not-allowed]="disabled()"
19
+ [disabled]="disabled()">
20
+ <div class="flex items-center gap-2">
21
+ <ng-content select="[icon]"></ng-content>
22
+ <ng-content></ng-content>
23
+ </div>
24
+ @if (hasSubmenu()) {
25
+ <span class="text-[10px] text-light-text-secondary opacity-50">▶</span>
26
+ }
27
+ </button>
28
+
29
+ @if (hasSubmenu() && $showSubmenu() && !disabled()) {
30
+ <div class="absolute left-full top-0 ml-[-2px] z-[60]">
31
+ <ng-content select="context-menu"></ng-content>
32
+ </div>
33
+ }
34
+ </div>
35
+ `
36
+ })
37
+ export class ContextButtonComponent {
38
+ danger = input(false, { transform: booleanAttribute });
39
+ disabled = input(false, { transform: booleanAttribute });
40
+ hasSubmenu = input(false, { transform: booleanAttribute });
41
+
42
+ $showSubmenu = signal(false);
43
+
44
+ onMouseEnter() {
45
+ if (this.hasSubmenu() && !this.disabled()) {
46
+ this.$showSubmenu.set(true);
47
+ }
48
+ }
49
+
50
+ onMouseLeave() {
51
+ if (this.hasSubmenu() && !this.disabled()) {
52
+ this.$showSubmenu.set(false);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,15 @@
1
+ import { Component } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ @Component({
5
+ selector: 'context-header',
6
+ standalone: true,
7
+ imports: [CommonModule],
8
+ template: `
9
+ <div
10
+ class="px-3 py-1.5 text-xs font-semibold text-light-text-secondary dark:text-dark-text-secondary truncate border-b border-light-border dark:border-dark-border mb-1">
11
+ <ng-content></ng-content>
12
+ </div>
13
+ `
14
+ })
15
+ export class ContextHeaderComponent { }
@@ -0,0 +1,26 @@
1
+ import { Directive, input, output, HostListener, ElementRef } from '@angular/core';
2
+ import { ContextMenuComponent } from './context-menu.component';
3
+
4
+ @Directive({
5
+ selector: '[appContextMenu]',
6
+ standalone: true
7
+ })
8
+ export class ContextMenuTriggerDirective {
9
+ appContextMenu = input.required<ContextMenuComponent>();
10
+
11
+ beforeOpen = output<void>();
12
+
13
+ constructor(private elementRef: ElementRef) { }
14
+
15
+ @HostListener('contextmenu', ['$event'])
16
+ onContextMenu(event: MouseEvent) {
17
+ event.preventDefault();
18
+ event.stopPropagation();
19
+
20
+ // Notify parent to prepare data
21
+ this.beforeOpen.emit();
22
+
23
+ // Show menu at cursor position
24
+ this.appContextMenu().show(event.clientX, event.clientY);
25
+ }
26
+ }
@@ -0,0 +1,95 @@
1
+ import { Component, input, output, ElementRef, HostListener, booleanAttribute, signal, ViewChild, AfterViewInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ @Component({
5
+ selector: 'context-menu',
6
+ standalone: true,
7
+ imports: [CommonModule],
8
+ template: `
9
+ @if ($isVisible() || $isSubmenu()) {
10
+ <div
11
+ #container
12
+ [class.fixed]="!$isSubmenu()"
13
+ [class.absolute]="$isSubmenu()"
14
+ [class.left-full]="$isSubmenu()"
15
+ [class.top-0]="$isSubmenu()"
16
+ [class.ml-[-2px]]="$isSubmenu()"
17
+ class="bg-white dark:bg-[#2a2a2a] border border-light-border dark:border-dark-border rounded shadow-lg z-50 min-w-[200px] text-light-text-primary dark:text-dark-text-primary py-1"
18
+ [style.left.px]="!$isSubmenu() ? $x() : null"
19
+ [style.top.px]="!$isSubmenu() ? $y() : null"
20
+ [style.visibility]="$isMeasuring() ? 'hidden' : 'visible'"
21
+ (click)="$event.stopPropagation()">
22
+ <ng-content></ng-content>
23
+ </div>
24
+ }
25
+ `
26
+ })
27
+ export class ContextMenuComponent {
28
+ $isSubmenu = input(false, { transform: booleanAttribute });
29
+
30
+ $isVisible = signal(false);
31
+ $isMeasuring = signal(false);
32
+ $x = signal(0);
33
+ $y = signal(0);
34
+
35
+ @ViewChild('container') container?: ElementRef<HTMLDivElement>;
36
+
37
+ close = output<void>();
38
+
39
+ constructor(private elementRef: ElementRef) { }
40
+
41
+ @HostListener('document:mousedown', ['$event'])
42
+ onDocumentClick(event: MouseEvent) {
43
+ if (this.$isSubmenu()) return;
44
+
45
+ // If visible and click is outside, close
46
+ if (this.$isVisible() && !this.elementRef.nativeElement.contains(event.target)) {
47
+ this.closeMenu();
48
+ }
49
+ }
50
+
51
+ show(x: number, y: number) {
52
+ if (this.$isSubmenu()) return;
53
+
54
+ this.$x.set(x);
55
+ this.$y.set(y);
56
+ this.$isMeasuring.set(true);
57
+ this.$isVisible.set(true);
58
+
59
+ // Wait for DOM to render the menu so we can measure it
60
+ setTimeout(() => {
61
+ if (this.container) {
62
+ const rect = this.container.nativeElement.getBoundingClientRect();
63
+ const width = rect.width;
64
+ const height = rect.height;
65
+
66
+ const windowWidth = window.innerWidth;
67
+ const windowHeight = window.innerHeight;
68
+
69
+ let newX = x;
70
+ let newY = y;
71
+
72
+ if (x + width > windowWidth)
73
+ newX = x - width;
74
+
75
+ if (y + height > windowHeight)
76
+ newY = y - height;
77
+
78
+ // Final safety check to ensure it doesn't go off the top/left edges
79
+ this.$x.set(Math.max(0, newX));
80
+ this.$y.set(Math.max(0, newY));
81
+ }
82
+ this.$isMeasuring.set(false);
83
+ }, 0);
84
+ }
85
+
86
+ hide() {
87
+ this.$isVisible.set(false);
88
+ this.$isMeasuring.set(false);
89
+ }
90
+
91
+ private closeMenu() {
92
+ this.hide();
93
+ this.close.emit();
94
+ }
95
+ }
@@ -0,0 +1,4 @@
1
+ export * from './context-menu.component';
2
+ export * from './context-header.component';
3
+ export * from './context-button.component';
4
+ export * from './context-menu-trigger.directive';