Fix/add npm jsr building (#722)
* fixed github workflows to improve handling of mutl runtimes * updating readme * Update packages/core/src/meshDevice.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/core/package.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/transport-http/package.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
+2
-20
@@ -18,23 +18,6 @@ or served from a node
|
||||
|
||||

|
||||
|
||||
## Progress Web App Support (PWA)
|
||||
|
||||
Meshtastic Web Client now includes Progressive Web App (PWA) functionality,
|
||||
allowing users to:
|
||||
|
||||
- Install the app on desktop and mobile devices
|
||||
- Access the interface offline
|
||||
- Receive updates automatically
|
||||
- Experience faster load times with caching
|
||||
|
||||
To install as a PWA:
|
||||
|
||||
- On desktop: Look for the install icon in your browser's address bar
|
||||
- On mobile: Use "Add to Home Screen" option in your browser menu
|
||||
|
||||
PWA functionality works with both the hosted version and self-hosted instances.
|
||||
|
||||
## Self-host
|
||||
|
||||
The client can be self hosted using the precompiled container images with an OCI
|
||||
@@ -56,7 +39,7 @@ Our release process follows these guidelines:
|
||||
|
||||
- **Versioning:** We use Semantic Versioning (`Major.Minor.Patch`).
|
||||
- **Stable Releases:** Published around the beginning of each month (e.g.,
|
||||
`v2.3.4`).
|
||||
`v2.6.1`).
|
||||
- **Pre-releases:** A pre-release is typically issued mid-month for testing and
|
||||
early adoption.
|
||||
- **Nightly Builds:** An experimental Docker image containing the latest
|
||||
@@ -106,6 +89,7 @@ instructions listed on the home page.
|
||||
Install the dependencies.
|
||||
|
||||
```bash
|
||||
cd packages/web &&
|
||||
bun install
|
||||
```
|
||||
|
||||
@@ -141,8 +125,6 @@ reasons:
|
||||
configuration, enhancing code quality and developer experience.
|
||||
- **Modern JavaScript**: First-class support for ESM imports, top-level await,
|
||||
and other modern JavaScript features.
|
||||
- **All-in-One Tooling**: Built-in package manager, bundler, test runner, and
|
||||
transpiler eliminate the need for multiple third-party tools.
|
||||
- **Node.js Compatibility**: Drop-in replacement for Node.js with better
|
||||
performance and built-in tooling.
|
||||
- **Reproducible Builds**: Lockfile ensures consistent builds across all
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
|
||||
"files": {
|
||||
"includes": ["**", "!node_modules/**", "!**/*.css", "!dist/**", "!build/**", "!coverage/**", "!**/*.d.ts"],
|
||||
"ignoreUnknown": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80,
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error",
|
||||
"noDebugger": "error"
|
||||
},
|
||||
"style": {
|
||||
"useConst": "error",
|
||||
"useBlockStatements": "error",
|
||||
"useSingleVarDeclarator": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "error",
|
||||
"noUnusedImports": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"semicolons": "always"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,7 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/meshtastic/web/issues"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "bun run check:fix"
|
||||
},
|
||||
|
||||
"homepage": "https://meshtastic.org",
|
||||
"scripts": {
|
||||
"build": "bunx --bun vite build",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@components/UI/Command.tsx";
|
||||
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
|
||||
import { useAppStore } from "@core/stores/appStore.ts";
|
||||
@@ -14,345 +14,345 @@ import { cn } from "@core/utils/cn.ts";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useCommandState } from "cmdk";
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
BoxSelectIcon,
|
||||
BugIcon,
|
||||
CloudOff,
|
||||
EraserIcon,
|
||||
FactoryIcon,
|
||||
LayersIcon,
|
||||
LinkIcon,
|
||||
type LucideIcon,
|
||||
MapIcon,
|
||||
MessageSquareIcon,
|
||||
Pin,
|
||||
PlusIcon,
|
||||
PowerIcon,
|
||||
QrCodeIcon,
|
||||
RefreshCwIcon,
|
||||
SettingsIcon,
|
||||
SmartphoneIcon,
|
||||
TrashIcon,
|
||||
UsersIcon,
|
||||
ArrowLeftRightIcon,
|
||||
BoxSelectIcon,
|
||||
BugIcon,
|
||||
CloudOff,
|
||||
EraserIcon,
|
||||
FactoryIcon,
|
||||
LayersIcon,
|
||||
LinkIcon,
|
||||
type LucideIcon,
|
||||
MapIcon,
|
||||
MessageSquareIcon,
|
||||
Pin,
|
||||
PlusIcon,
|
||||
PowerIcon,
|
||||
QrCodeIcon,
|
||||
RefreshCwIcon,
|
||||
SettingsIcon,
|
||||
SmartphoneIcon,
|
||||
TrashIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface Group {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
commands: Command[];
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
commands: Command[];
|
||||
}
|
||||
export interface Command {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
action?: () => void;
|
||||
subItems?: SubItem[];
|
||||
tags?: string[];
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
action?: () => void;
|
||||
subItems?: SubItem[];
|
||||
tags?: string[];
|
||||
}
|
||||
export interface SubItem {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
action: () => void;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
export const CommandPalette = () => {
|
||||
const {
|
||||
commandPaletteOpen,
|
||||
setCommandPaletteOpen,
|
||||
setConnectDialogOpen,
|
||||
setSelectedDevice,
|
||||
} = useAppStore();
|
||||
const { getDevices } = useDeviceStore();
|
||||
const { setDialogOpen, getNode, connection } = useDevice();
|
||||
const { pinnedItems, togglePinnedItem } = usePinnedItems({
|
||||
storageName: "pinnedCommandMenuGroups",
|
||||
});
|
||||
const { t } = useTranslation("commandPalette");
|
||||
const navigate = useNavigate({ from: "/" });
|
||||
const {
|
||||
commandPaletteOpen,
|
||||
setCommandPaletteOpen,
|
||||
setConnectDialogOpen,
|
||||
setSelectedDevice,
|
||||
} = useAppStore();
|
||||
const { getDevices } = useDeviceStore();
|
||||
const { setDialogOpen, getNode, connection } = useDevice();
|
||||
const { pinnedItems, togglePinnedItem } = usePinnedItems({
|
||||
storageName: "pinnedCommandMenuGroups",
|
||||
});
|
||||
const { t } = useTranslation("commandPalette");
|
||||
const navigate = useNavigate({ from: "/" });
|
||||
|
||||
const groups: Group[] = [
|
||||
{
|
||||
id: "gotoGroup",
|
||||
label: t("goto.label"),
|
||||
icon: LinkIcon,
|
||||
commands: [
|
||||
{
|
||||
label: t("goto.command.messages"),
|
||||
icon: MessageSquareIcon,
|
||||
action() {
|
||||
navigate({ to: "/messages" });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("goto.command.map"),
|
||||
icon: MapIcon,
|
||||
action() {
|
||||
navigate({ to: "/map" });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("goto.command.config"),
|
||||
icon: SettingsIcon,
|
||||
action() {
|
||||
navigate({ to: "/config" });
|
||||
},
|
||||
tags: ["settings"],
|
||||
},
|
||||
{
|
||||
label: t("goto.command.channels"),
|
||||
icon: LayersIcon,
|
||||
action() {
|
||||
navigate({ to: "/channels" });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("goto.command.nodes"),
|
||||
icon: UsersIcon,
|
||||
action() {
|
||||
navigate({ to: "/nodes" });
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "manageGroup",
|
||||
label: t("manage.label"),
|
||||
icon: SmartphoneIcon,
|
||||
commands: [
|
||||
{
|
||||
label: t("manage.command.switchNode"),
|
||||
icon: ArrowLeftRightIcon,
|
||||
subItems: getDevices().map((device) => ({
|
||||
label:
|
||||
getNode(device.hardware.myNodeNum)?.user?.longName ??
|
||||
t("unknown.shortName"),
|
||||
icon: (
|
||||
<Avatar
|
||||
text={
|
||||
getNode(device.hardware.myNodeNum)?.user?.shortName ??
|
||||
t("unknown.shortName")
|
||||
}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
setSelectedDevice(device.id);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: t("manage.command.connectNewNode"),
|
||||
icon: PlusIcon,
|
||||
action() {
|
||||
setConnectDialogOpen(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "contextualGroup",
|
||||
label: t("contextual.label"),
|
||||
icon: BoxSelectIcon,
|
||||
commands: [
|
||||
{
|
||||
label: t("contextual.command.qrCode"),
|
||||
icon: QrCodeIcon,
|
||||
subItems: [
|
||||
{
|
||||
label: t("contextual.command.qrGenerator"),
|
||||
icon: <QrCodeIcon size={16} />,
|
||||
action() {
|
||||
setDialogOpen("QR", true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.qrImport"),
|
||||
icon: <QrCodeIcon size={16} />,
|
||||
action() {
|
||||
setDialogOpen("import", true);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.scheduleShutdown"),
|
||||
icon: PowerIcon,
|
||||
action() {
|
||||
setDialogOpen("shutdown", true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.scheduleReboot"),
|
||||
icon: RefreshCwIcon,
|
||||
action() {
|
||||
setDialogOpen("reboot", true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.rebootToOtaMode"),
|
||||
icon: RefreshCwIcon,
|
||||
action() {
|
||||
setDialogOpen("rebootOTA", true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.resetNodeDb"),
|
||||
icon: TrashIcon,
|
||||
action() {
|
||||
connection?.resetNodes();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.disconnect"),
|
||||
icon: CloudOff,
|
||||
action() {
|
||||
connection?.disconnect().catch((error) => {
|
||||
console.error("Failed to disconnect:", error);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.factoryResetDevice"),
|
||||
icon: FactoryIcon,
|
||||
action() {
|
||||
connection?.factoryResetDevice();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.factoryResetConfig"),
|
||||
icon: FactoryIcon,
|
||||
action() {
|
||||
connection?.factoryResetConfig();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "debugGroup",
|
||||
label: t("debug.label"),
|
||||
icon: BugIcon,
|
||||
commands: [
|
||||
{
|
||||
label: t("debug.command.reconfigure"),
|
||||
icon: RefreshCwIcon,
|
||||
action() {
|
||||
void connection?.configure();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("debug.command.clearAllStoredMessages"),
|
||||
icon: EraserIcon,
|
||||
action() {
|
||||
setDialogOpen("deleteMessages", true);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const groups: Group[] = [
|
||||
{
|
||||
id: "gotoGroup",
|
||||
label: t("goto.label"),
|
||||
icon: LinkIcon,
|
||||
commands: [
|
||||
{
|
||||
label: t("goto.command.messages"),
|
||||
icon: MessageSquareIcon,
|
||||
action() {
|
||||
navigate({ to: "/messages" });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("goto.command.map"),
|
||||
icon: MapIcon,
|
||||
action() {
|
||||
navigate({ to: "/map" });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("goto.command.config"),
|
||||
icon: SettingsIcon,
|
||||
action() {
|
||||
navigate({ to: "/config" });
|
||||
},
|
||||
tags: ["settings"],
|
||||
},
|
||||
{
|
||||
label: t("goto.command.channels"),
|
||||
icon: LayersIcon,
|
||||
action() {
|
||||
navigate({ to: "/channels" });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("goto.command.nodes"),
|
||||
icon: UsersIcon,
|
||||
action() {
|
||||
navigate({ to: "/nodes" });
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "manageGroup",
|
||||
label: t("manage.label"),
|
||||
icon: SmartphoneIcon,
|
||||
commands: [
|
||||
{
|
||||
label: t("manage.command.switchNode"),
|
||||
icon: ArrowLeftRightIcon,
|
||||
subItems: getDevices().map((device) => ({
|
||||
label:
|
||||
getNode(device.hardware.myNodeNum)?.user?.longName ??
|
||||
t("unknown.shortName"),
|
||||
icon: (
|
||||
<Avatar
|
||||
text={
|
||||
getNode(device.hardware.myNodeNum)?.user?.shortName ??
|
||||
t("unknown.shortName")
|
||||
}
|
||||
/>
|
||||
),
|
||||
action() {
|
||||
setSelectedDevice(device.id);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: t("manage.command.connectNewNode"),
|
||||
icon: PlusIcon,
|
||||
action() {
|
||||
setConnectDialogOpen(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "contextualGroup",
|
||||
label: t("contextual.label"),
|
||||
icon: BoxSelectIcon,
|
||||
commands: [
|
||||
{
|
||||
label: t("contextual.command.qrCode"),
|
||||
icon: QrCodeIcon,
|
||||
subItems: [
|
||||
{
|
||||
label: t("contextual.command.qrGenerator"),
|
||||
icon: <QrCodeIcon size={16} />,
|
||||
action() {
|
||||
setDialogOpen("QR", true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.qrImport"),
|
||||
icon: <QrCodeIcon size={16} />,
|
||||
action() {
|
||||
setDialogOpen("import", true);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.scheduleShutdown"),
|
||||
icon: PowerIcon,
|
||||
action() {
|
||||
setDialogOpen("shutdown", true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.scheduleReboot"),
|
||||
icon: RefreshCwIcon,
|
||||
action() {
|
||||
setDialogOpen("reboot", true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.rebootToOtaMode"),
|
||||
icon: RefreshCwIcon,
|
||||
action() {
|
||||
setDialogOpen("rebootOTA", true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.resetNodeDb"),
|
||||
icon: TrashIcon,
|
||||
action() {
|
||||
connection?.resetNodes();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.disconnect"),
|
||||
icon: CloudOff,
|
||||
action() {
|
||||
connection?.disconnect().catch((error) => {
|
||||
console.error("Failed to disconnect:", error);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.factoryResetDevice"),
|
||||
icon: FactoryIcon,
|
||||
action() {
|
||||
connection?.factoryResetDevice();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("contextual.command.factoryResetConfig"),
|
||||
icon: FactoryIcon,
|
||||
action() {
|
||||
connection?.factoryResetConfig();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "debugGroup",
|
||||
label: t("debug.label"),
|
||||
icon: BugIcon,
|
||||
commands: [
|
||||
{
|
||||
label: t("debug.command.reconfigure"),
|
||||
icon: RefreshCwIcon,
|
||||
action() {
|
||||
void connection?.configure();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("debug.command.clearAllStoredMessages"),
|
||||
icon: EraserIcon,
|
||||
action() {
|
||||
setDialogOpen("deleteMessages", true);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const sortedGroups = [...groups].sort((a, b) => {
|
||||
const aPinned = pinnedItems.includes(a.id) ? 1 : 0;
|
||||
const bPinned = pinnedItems.includes(b.id) ? 1 : 0;
|
||||
return bPinned - aPinned;
|
||||
});
|
||||
const sortedGroups = [...groups].sort((a, b) => {
|
||||
const aPinned = pinnedItems.includes(a.id) ? 1 : 0;
|
||||
const bPinned = pinnedItems.includes(b.id) ? 1 : 0;
|
||||
return bPinned - aPinned;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setCommandPaletteOpen(true);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setCommandPaletteOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
globalThis.addEventListener("keydown", handleKeydown);
|
||||
return () => globalThis.removeEventListener("keydown", handleKeydown);
|
||||
}, [setCommandPaletteOpen]);
|
||||
globalThis.addEventListener("keydown", handleKeydown);
|
||||
return () => globalThis.removeEventListener("keydown", handleKeydown);
|
||||
}, [setCommandPaletteOpen]);
|
||||
|
||||
return (
|
||||
<CommandDialog
|
||||
open={commandPaletteOpen}
|
||||
onOpenChange={setCommandPaletteOpen}
|
||||
>
|
||||
<CommandInput placeholder={t("search.commandPalette")} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{t("emptyState")}</CommandEmpty>
|
||||
{sortedGroups.map((group) => (
|
||||
<CommandGroup
|
||||
key={group.label}
|
||||
heading={
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{group.label}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePinnedItem(group.id)}
|
||||
className={cn(
|
||||
"transition-all duration-300 scale-100 cursor-pointer p-2 focus:*:data-label:opacity-100",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
data-label
|
||||
className="transition-all block absolute w-full mb-auto mt-auto ml-0 mr-0 text-xs left-0 -top-5 opacity-0 rounded-lg"
|
||||
/>
|
||||
<Pin
|
||||
size={16}
|
||||
className={cn(
|
||||
"transition-opacity",
|
||||
pinnedItems.includes(group.id)
|
||||
? "opacity-100 text-red-500"
|
||||
: "opacity-40 hover:opacity-70",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{group.commands.map((command) => (
|
||||
<div key={command.label}>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
command.action?.();
|
||||
setCommandPaletteOpen(false);
|
||||
}}
|
||||
>
|
||||
<command.icon size={16} className="mr-2" />
|
||||
{command.label}
|
||||
</CommandItem>
|
||||
{command.subItems?.map((subItem) => (
|
||||
<SubItem
|
||||
key={subItem.label}
|
||||
label={subItem.label}
|
||||
icon={subItem.icon}
|
||||
action={subItem.action}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
return (
|
||||
<CommandDialog
|
||||
open={commandPaletteOpen}
|
||||
onOpenChange={setCommandPaletteOpen}
|
||||
>
|
||||
<CommandInput placeholder={t("search.commandPalette")} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{t("emptyState")}</CommandEmpty>
|
||||
{sortedGroups.map((group) => (
|
||||
<CommandGroup
|
||||
key={group.label}
|
||||
heading={
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{group.label}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePinnedItem(group.id)}
|
||||
className={cn(
|
||||
"transition-all duration-300 scale-100 cursor-pointer p-2 focus:*:data-label:opacity-100",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
data-label
|
||||
className="transition-all block absolute w-full mb-auto mt-auto ml-0 mr-0 text-xs left-0 -top-5 opacity-0 rounded-lg"
|
||||
/>
|
||||
<Pin
|
||||
size={16}
|
||||
className={cn(
|
||||
"transition-opacity",
|
||||
pinnedItems.includes(group.id)
|
||||
? "opacity-100 text-red-500"
|
||||
: "opacity-40 hover:opacity-70",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{group.commands.map((command) => (
|
||||
<div key={command.label}>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
command.action?.();
|
||||
setCommandPaletteOpen(false);
|
||||
}}
|
||||
>
|
||||
<command.icon size={16} className="mr-2" />
|
||||
{command.label}
|
||||
</CommandItem>
|
||||
{command.subItems?.map((subItem) => (
|
||||
<SubItem
|
||||
key={subItem.label}
|
||||
label={subItem.label}
|
||||
icon={subItem.icon}
|
||||
action={subItem.action}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const SubItem = ({
|
||||
label,
|
||||
icon,
|
||||
action,
|
||||
label,
|
||||
icon,
|
||||
action,
|
||||
}: {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
action: () => void;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
action: () => void;
|
||||
}) => {
|
||||
const search = useCommandState((state) => state.search);
|
||||
if (!search) {
|
||||
return null;
|
||||
}
|
||||
const search = useCommandState((state) => state.search);
|
||||
if (!search) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandItem onSelect={action}>
|
||||
{icon}
|
||||
{label}
|
||||
</CommandItem>
|
||||
);
|
||||
return (
|
||||
<CommandItem onSelect={action}>
|
||||
{icon}
|
||||
{label}
|
||||
</CommandItem>
|
||||
);
|
||||
};
|
||||
|
||||
Vendored
+4
-4
@@ -1,11 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly env: {
|
||||
readonly VITE_COMMIT_HASH: string;
|
||||
};
|
||||
readonly env: {
|
||||
readonly VITE_COMMIT_HASH: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
+46
-46
@@ -7,56 +7,56 @@ import { defineConfig } from "vite";
|
||||
|
||||
let hash = "";
|
||||
try {
|
||||
hash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim();
|
||||
hash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim();
|
||||
} catch (error) {
|
||||
console.error("Error getting git hash:", error);
|
||||
hash = "DEV";
|
||||
console.error("Error getting git hash:", error);
|
||||
hash = "DEV";
|
||||
}
|
||||
|
||||
const CONTENT_SECURITY_POLICY =
|
||||
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' data: https://rsms.me https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' data: https://rsms.me https://cdn.jsdelivr.net; worker-src 'self' blob:; object-src 'none'; base-uri 'self';";
|
||||
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' data: https://rsms.me https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' data: https://rsms.me https://cdn.jsdelivr.net; worker-src 'self' blob:; object-src 'none'; base-uri 'self';";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
// VitePWA({
|
||||
// registerType: "autoUpdate",
|
||||
// strategies: "generateSW",
|
||||
// devOptions: {
|
||||
// enabled: true,
|
||||
// },
|
||||
// workbox: {
|
||||
// cleanupOutdatedCaches: true,
|
||||
// sourcemap: true,
|
||||
// },
|
||||
// }),
|
||||
],
|
||||
optimizeDeps: {
|
||||
include: ["react/jsx-runtime"],
|
||||
},
|
||||
define: {
|
||||
"import.meta.env.VITE_COMMIT_HASH": JSON.stringify(hash),
|
||||
},
|
||||
build: {
|
||||
emptyOutDir: true,
|
||||
assetsDir: "./",
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@app": path.resolve(process.cwd(), "./src"),
|
||||
"@pages": path.resolve(process.cwd(), "./src/pages"),
|
||||
"@components": path.resolve(process.cwd(), "./src/components"),
|
||||
"@core": path.resolve(process.cwd(), "./src/core"),
|
||||
"@layouts": path.resolve(process.cwd(), "./src/layouts"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
headers: {
|
||||
"content-security-policy": CONTENT_SECURITY_POLICY,
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
// VitePWA({
|
||||
// registerType: "autoUpdate",
|
||||
// strategies: "generateSW",
|
||||
// devOptions: {
|
||||
// enabled: true,
|
||||
// },
|
||||
// workbox: {
|
||||
// cleanupOutdatedCaches: true,
|
||||
// sourcemap: true,
|
||||
// },
|
||||
// }),
|
||||
],
|
||||
optimizeDeps: {
|
||||
include: ["react/jsx-runtime"],
|
||||
},
|
||||
define: {
|
||||
"import.meta.env.VITE_COMMIT_HASH": JSON.stringify(hash),
|
||||
},
|
||||
build: {
|
||||
emptyOutDir: true,
|
||||
assetsDir: "./",
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@app": path.resolve(process.cwd(), "./src"),
|
||||
"@pages": path.resolve(process.cwd(), "./src/pages"),
|
||||
"@components": path.resolve(process.cwd(), "./src/components"),
|
||||
"@core": path.resolve(process.cwd(), "./src/core"),
|
||||
"@layouts": path.resolve(process.cwd(), "./src/layouts"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
headers: {
|
||||
"content-security-policy": CONTENT_SECURITY_POLICY,
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,25 +7,25 @@ import { enableMapSet } from "immer";
|
||||
|
||||
enableMapSet();
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@app": path.resolve(process.cwd(), "./src"),
|
||||
"@public": path.resolve(process.cwd(), "./public"),
|
||||
"@core": path.resolve(process.cwd(), "./src/core"),
|
||||
"@pages": path.resolve(process.cwd(), "./src/pages"),
|
||||
"@components": path.resolve(process.cwd(), "./src/components"),
|
||||
"@layouts": path.resolve(process.cwd(), "./src/layouts"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "happy-dom",
|
||||
globals: true,
|
||||
mockReset: true,
|
||||
clearMocks: true,
|
||||
restoreMocks: true,
|
||||
root: path.resolve(process.cwd(), "./src"),
|
||||
include: ["**/*.{test,spec}.{ts,tsx}"],
|
||||
setupFiles: ["./src/tests/setup.ts"],
|
||||
},
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@app": path.resolve(process.cwd(), "./src"),
|
||||
"@public": path.resolve(process.cwd(), "./public"),
|
||||
"@core": path.resolve(process.cwd(), "./src/core"),
|
||||
"@pages": path.resolve(process.cwd(), "./src/pages"),
|
||||
"@components": path.resolve(process.cwd(), "./src/components"),
|
||||
"@layouts": path.resolve(process.cwd(), "./src/layouts"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "happy-dom",
|
||||
globals: true,
|
||||
mockReset: true,
|
||||
clearMocks: true,
|
||||
restoreMocks: true,
|
||||
root: path.resolve(process.cwd(), "./src"),
|
||||
include: ["**/*.{test,spec}.{ts,tsx}"],
|
||||
setupFiles: ["./src/tests/setup.ts"],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user