Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
- Always comply with ARIA accessibility standards and make sure all inputs have an id and name field. If you are working on a file which doesn't comply with ARIA standards, add the necessary ARIA fields as part of user request, even if the changes does not fall within scope
- Do not inline typescript types and interfaces - move them to types folder
- When working on a file, and it is more than 1000 lines of code then refactor it into smaller, easier to test and maintain files. There are no exceptions to this. Always do this even if it is outside the scope of what was asked
- Always run `bun run lint` and `bun run build` after making code changes to verify that the build succeeds and there are no linting issues
- Always run `bun run lint` and `bun run build` after making code changes to verify there are no linting or build issues
- Be patient with `bun run build`. It can take minutes to run
- Always use this folder structure:

src/
Expand Down
20 changes: 4 additions & 16 deletions TODO.txt
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
Lospec dialog
Should not download from API. Fetch directly from server. Cache Responses
Add Pagination
Tags should be clickable
Show all colors when clicking plus button
Colors should have tooltips
Check all AI Asset generation
-------------

Select Project Loading screen
Better CI/CD Dev -> Prod workflow
Map Management -> Edit Maps -> Allow editing map title
World View

AI Asset Generation:
Recheck out all native implementations work
History
Gallery
Scheduler
Add depleting progress bar to show remaining quota
Ability to download/add to tileset/open in image Editor
Tools -> MCP Server

Ad Marketplace for selling/buying tilesets etc.
Exploration:
Expand Down
4 changes: 2 additions & 2 deletions public/_headers
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
# style-src: unsafe-inline required for Radix UI / Tailwind inline styles
# img-src: data: / blob: for canvas exports; https: for remote images
# font-src: self-hosted app fonts + data: URIs
# connect-src: Sentry ingest + Cloudflare Insights RUM endpoint + app APIs
# connect-src: Sentry ingest + analytics + app APIs + browser-side AI providers
# worker-src: blob: for Vite-generated web workers
# object-src: none (no Flash / plugins)
# base-uri: self (prevent base-tag injection)
# form-action: self (no external form submissions)
# frame-ancestors: none (blocks clickjacking; supersedes X-Frame-Options)
# upgrade-insecure-requests: force HTTP→HTTPS for sub-resources
Content-Security-Policy: default-src 'self'; script-src 'self' https://static.cloudflareinsights.com https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https://o4510891797250048.ingest.us.sentry.io https://*.sentry.io https://cloudflareinsights.com https://www.google-analytics.com https://*.google-analytics.com https://www.google.com https://api.2dtiler.com https://api.openai.com https://api.together.xyz https://api.x.ai; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
Content-Security-Policy: default-src 'self'; script-src 'self' https://static.cloudflareinsights.com https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https://o4510891797250048.ingest.us.sentry.io https://*.sentry.io https://cloudflareinsights.com https://www.google-analytics.com https://*.google-analytics.com https://www.google.com https://api.2dtiler.com https://api.openai.com https://generativelanguage.googleapis.com https://api.together.xyz https://api.x.ai https://router.huggingface.co; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
10 changes: 10 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
getProject,
loadProjectPrefs,
loadLastProjectId,
loadLospecPaletteSyncEnabled,
} from "@/services/db";
import {
hydrateZoomStoreForProject,
saveCurrentProjectPrefs,
} from "@/features/project-management/lib/project-prefs";
import { getActiveTilesetTileSize } from "@/features/project-management/lib/project";
import { startLospecPaletteBackgroundSync } from "@/features/image-editor/lib/lospec-sync-controller";
import { zoomStore } from "@/store/zoom-store";
import type { ToolName } from "@/features/app-shell";
import { Toaster } from "@/components/ui/Sonner";
Expand Down Expand Up @@ -51,6 +53,14 @@ function App() {
const [bugReportOpen, setBugReportOpen] = useState(false);
const [activeTool, setActiveTool] = useState<ToolName | null>(null);

useEffect(() => {
if (!loadLospecPaletteSyncEnabled()) {
return;
}

void startLospecPaletteBackgroundSync();
}, []);

useEffect(() => {
if (storeInitStarted) return;
storeInitStarted = true;
Expand Down
209 changes: 165 additions & 44 deletions src/components/dialogs/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import {
DialogTitle,
} from "@/components/ui/Dialog";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/Accordion";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/Select";
import { Switch } from "@/components/ui/Switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/Tabs";
import { Label } from "@/components/ui/Label";
import { Button } from "@/components/ui/Button";
import { getSettings, saveSettings } from "@/services/db";
Expand All @@ -26,9 +28,24 @@ import {
import type {
SettingsDialogProps,
SettingsKeyRowProps as KeyRowProps,
SettingsSection,
SettingsSectionId,
} from "@/features/app-shell";
import type { AppSettings } from "@/types";

const SETTINGS_SECTIONS: SettingsSection[] = [
{
id: "general",
label: "General",
description: "Project behavior and application defaults.",
},
{
id: "api-keys",
label: "API Keys",
description: "Provider credentials used for AI image generation.",
},
];

function ApiKeyRow({ id, label, url, placeholder }: KeyRowProps) {
const [value, setValue] = useState("");
const [visible, setVisible] = useState(false);
Expand Down Expand Up @@ -143,9 +160,16 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
const [settings, setSettings] = useState<AppSettings>({
autoSaveEnabled: true,
});
const [activeSection, setActiveSection] =
useState<SettingsSectionId>("general");

const handleSectionChange = (value: string) => {
setActiveSection(value as SettingsSectionId);
};

useEffect(() => {
if (open) {
setActiveSection("general");
getSettings().then((newSettings) => {
setSettings(newSettings);
});
Expand All @@ -160,52 +184,149 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogContent className="flex h-[min(85vh,720px)] min-h-0 flex-col gap-0 overflow-hidden p-0 sm:max-w-3xl">
<DialogHeader className="shrink-0 gap-0">
<div className="border-b border-border-visible px-6 pt-6 pb-5">
<DialogTitle>Settings</DialogTitle>
</div>
<DialogDescription className="sr-only">
Application settings
</DialogDescription>
</DialogHeader>
<Accordion type="multiple" defaultValue={["general"]}>
<AccordionItem value="general">
<AccordionTrigger>General</AccordionTrigger>
<AccordionContent>
<div className="flex items-center justify-between">
<Label htmlFor="autosave" className="text-sm">
Save project every minute
</Label>
<Switch
id="autosave"
checked={settings.autoSaveEnabled}
onCheckedChange={handleToggleAutoSave}
/>
<Tabs
value={activeSection}
onValueChange={handleSectionChange}
orientation="vertical"
className="min-h-0 flex-1 flex-col items-stretch gap-0 overflow-hidden sm:flex-row"
>
<aside className="border-b border-border-visible px-4 py-4 sm:flex sm:h-full sm:w-56 sm:flex-col sm:self-stretch sm:border-r sm:border-b-0 sm:px-3 sm:py-5">
<div className="space-y-2 sm:hidden">
<Label
htmlFor="settings-section-select"
className="text-xs font-medium"
>
Section
</Label>
<Select
name="settings-section"
value={activeSection}
onValueChange={handleSectionChange}
>
<SelectTrigger
id="settings-section-select"
aria-label="Settings section"
className="h-11 w-full rounded-xl px-3 text-left"
>
<SelectValue placeholder="Select a section" />
</SelectTrigger>
<SelectContent>
{SETTINGS_SECTIONS.map((section) => (
<SelectItem key={section.id} value={section.id}>
{section.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<TabsList
aria-label="Settings sections"
className="hidden w-full items-stretch justify-start self-start gap-1 bg-transparent p-0 sm:flex sm:h-full"
variant="line"
>
{SETTINGS_SECTIONS.map((section) => (
<TabsTrigger
key={section.id}
value={section.id}
className="min-h-11 rounded-xl px-3 py-2 text-left whitespace-normal after:hidden"
>
<span className="flex min-w-0 flex-col items-start">
<span>{section.label}</span>
<span className="text-[10px] leading-relaxed text-muted-foreground whitespace-normal break-words">
{section.description}
</span>
</span>
</TabsTrigger>
))}
</TabsList>
</aside>

<div className="min-h-0 flex-1 overflow-hidden px-6 py-5">
<TabsContent
value="general"
className="mt-0 h-full overflow-y-auto"
>
<div className="space-y-5">
<div>
<h2 className="text-sm font-medium text-foreground">
General
</h2>
<p className="mt-1 hidden text-xs leading-relaxed text-muted-foreground sm:block">
Control how the app handles project saving.
</p>
</div>
<section
aria-labelledby="settings-general-autosave-label"
className="rounded-xl border border-border-visible p-4"
>
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<Label
id="settings-general-autosave-label"
htmlFor="autosave"
className="text-sm"
>
Save project every minute
</Label>
<p
id="settings-general-autosave-description"
className="text-xs leading-relaxed text-muted-foreground"
>
Automatically persists your current project in the
background.
</p>
</div>
<Switch
id="autosave"
name="autosave"
checked={settings.autoSaveEnabled}
onCheckedChange={handleToggleAutoSave}
aria-describedby="settings-general-autosave-description"
/>
</div>
</section>
</div>
</AccordionContent>
</AccordionItem>
</TabsContent>

<AccordionItem value="ai-keys">
<AccordionTrigger>AI API Keys</AccordionTrigger>
<AccordionContent>
<div className="space-y-3">
<p className="text-[11px] text-muted-foreground leading-relaxed">
Keys are obfuscated locally in your browser. They are never
sent to any server other than the provider&apos;s own API, but
any script running on this origin can still access them.
</p>
{API_KEY_PROVIDERS.map((p) => (
<ApiKeyRow
key={p.id}
id={p.id}
label={p.label}
url={p.url}
placeholder={p.placeholder}
/>
))}
<TabsContent
value="api-keys"
className="mt-0 h-full overflow-y-auto"
>
<div className="space-y-5">
<div>
<h2 className="text-sm font-medium text-foreground">
API Keys
</h2>
<p className="mt-1 hidden text-xs leading-relaxed text-muted-foreground sm:block">
Keys are obfuscated locally in your browser. They are never
sent to any server other than the provider&apos;s own API,
but any script running on this origin can still access them.
</p>
</div>
<div className="space-y-3">
{API_KEY_PROVIDERS.map((p) => (
<ApiKeyRow
key={p.id}
id={p.id}
label={p.label}
url={p.url}
placeholder={p.placeholder}
/>
))}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</TabsContent>
</div>
</Tabs>
</DialogContent>
</Dialog>
);
Expand Down
6 changes: 6 additions & 0 deletions src/config/api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
import type { ApiKeyProvider } from "@/types/integrations/api-keys";

export const API_KEY_PROVIDERS: ApiKeyProvider[] = [
{
id: "huggingface",
label: "Hugging Face",
url: "https://huggingface.co/settings/tokens/new?ownUserPermissions=inference.serverless.write&tokenType=fineGrained",
placeholder: "hf_...",
},
{
id: "openai",
label: "OpenAI",
Expand Down
23 changes: 23 additions & 0 deletions src/config/content-security-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const APP_CONNECT_SOURCES = [
"'self'",
"https://o4510891797250048.ingest.us.sentry.io",
"https://*.sentry.io",
"https://cloudflareinsights.com",
"https://www.google-analytics.com",
"https://*.google-analytics.com",
"https://www.google.com",
"https://api.2dtiler.com",
] as const;

export const AI_PROVIDER_CONNECT_SOURCES = [
"https://api.openai.com",
"https://generativelanguage.googleapis.com",
"https://api.together.xyz",
"https://api.x.ai",
"https://router.huggingface.co",
] as const;

export const CONNECT_SOURCES = [
...APP_CONNECT_SOURCES,
...AI_PROVIDER_CONNECT_SOURCES,
] as const;
Loading