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.
- package/LICENSE +159 -0
- package/README.md +17 -0
- package/async/README.md +46 -0
- package/async/async.ts +16 -0
- package/async/cancellation-token.ts +90 -0
- package/dev/README.md +41 -0
- package/dev/console-hook.ts +25 -0
- package/dev/debug.service.ts.example +29 -0
- package/framework/README.md +34 -0
- package/framework/services-init.ts +25 -0
- package/index.ts +21 -0
- package/localization/README.md +73 -0
- package/localization/localization.interface.ts +18 -0
- package/localization/localization.service.ts +131 -0
- package/localization/localize.pipe.ts +30 -0
- package/log/README.md +275 -0
- package/log/echo-provider.ts +27 -0
- package/log/echo.ts +635 -0
- package/log/index.ts +6 -0
- package/log/log-systems.ts +20 -0
- package/navigation/README.md +47 -0
- package/navigation/back-on-click.directive.ts +19 -0
- package/navigation/index.ts +3 -0
- package/navigation/navigation-stack.service.ts +33 -0
- package/package.json +29 -0
- package/storage/README.md +75 -0
- package/storage/capacitor-files.service.ts +38 -0
- package/storage/file-box.service.ts +112 -0
- package/storage/files.ts +42 -0
- package/storage/key-signals.ts +49 -0
- package/storage/local-storage-files.service.ts +49 -0
- package/storage/settings-signals.service.ts +24 -0
- package/storage/settings.service.ts +24 -0
- package/storage/tauri-files.service.ts +69 -0
- package/theme/README.md +44 -0
- package/theme/theme.service.ts +33 -0
- package/ui/README.md +42 -0
- package/ui/context-menu/context-button.component.ts +55 -0
- package/ui/context-menu/context-header.component.ts +15 -0
- package/ui/context-menu/context-menu-trigger.directive.ts +26 -0
- package/ui/context-menu/context-menu.component.ts +95 -0
- 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
|
+
}
|