refactor: upgrade icon strings to structured objects

This commit is contained in:
N1C4T
2026-02-23 15:42:39 +04:00
parent f9c7435ed1
commit 3934ec0650
4 changed files with 58 additions and 32 deletions

View File

@@ -33,7 +33,8 @@
## Project Overview
* `src/lib/data.ts`: Main registry for applications and distributions.
* `src/lib/apps/*.json`: Main registry for applications (split by category).
* `src/lib/data.ts`: Main registry for distributions, categories, and Typescript types.
* `src/lib/aur-packages.json`: Whitelist for AUR packages that lack standard suffixes.
* `src/lib/nix-unfree.json`: Registry for unfree Nix packages.
* `src/lib/verified-flatpaks.json`: Auto-generated list of verified Flathub apps. **Do not edit.**
@@ -100,7 +101,7 @@ docker run -it --rm fedora:latest bash -c "dnf check-update; bash"
## Adding Applications
All applications are defined in [`src/lib/data.ts`](src/lib/data.ts).
All applications are defined in category-specific JSON files within [`src/lib/apps/`](src/lib/apps/).
### 1. Mandatory Research Protocol
@@ -122,22 +123,26 @@ All applications are defined in [`src/lib/data.ts`](src/lib/data.ts).
### 2. Entry Structure
```typescript
```json
{
id: 'app-id', // Unique, lowercase, kebab-case
name: 'App Name', // Official display name
description: 'Short description', // Max ~60 characters
category: 'Category', // Must match valid categories
iconUrl: si('icon-slug', '#color'), // See Icon System section
targets: {
ubuntu: 'exact-package-name', // apt package (official repos ONLY)
arch: 'exact-package-name', // pacman OR AUR package name
flatpak: 'com.vendor.AppId', // FULL Flatpak App ID (reverse DNS)
snap: 'snap-name', // Add --classic if needed
homebrew: 'formula-name', // Formula (CLI) or '--cask name' (GUI)
// ... add other distros
"id": "app-id", // Unique, lowercase, kebab-case
"name": "App Name", // Official display name
"description": "Short description", // Max ~60 characters
"category": "Category", // Must match valid categories
"icon": { // See Icon System section
"type": "iconify",
"set": "simple-icons",
"name": "python",
"color": "#3776AB"
},
unavailableReason?: 'Markdown install instructions'
"targets": {
"ubuntu": "exact-package-name", // apt package (official repos ONLY)
"arch": "exact-package-name", // pacman OR AUR package name
"flatpak": "com.vendor.AppId", // FULL Flatpak App ID (reverse DNS)
"snap": "snap-name", // Add --classic if needed
"homebrew": "formula-name" // Formula (CLI) or '--cask name' (GUI)
},
"unavailableReason": "Markdown install instructions"
}
```
@@ -171,7 +176,7 @@ This field renders Markdown when a target is missing. It serves as the manual fa
Nixpkgs requires explicit user consent for unfree software.
1. Check the license on [search.nixos.org](https://search.nixos.org/packages).
2. If the license is unfree, add it to `src/lib/nix-unfree.json` if missing.
3. Add the package to `data.ts` normally.
3. Add the package to the appropriate JSON file in `src/lib/apps/` normally.
#### Ubuntu/Debian
**Strict Repository Policy**: The generation scripts do **not** enable extra repositories (like PPAs or `non-free` by default). Packages must be available in the standard enabled repositories.
@@ -214,18 +219,40 @@ Homebrew (macOS/Linux) has two package types. Check [formulae.brew.sh](https://f
### 5. Icon System
We use [Iconify](https://iconify.design/).
Every app needs an icon! Our JSON format makes it super simple to add icons using [Iconify](https://iconify.design/).
**Helper Functions:**
* `si('slug', '#color')` for **Simple Icons** (Brands).
* `lo('slug')` for **Logos** (Multi-colored).
* `mdi('name', '#color')` for **Material Design**.
We store icons as structured objects in the JSON. You just need to provide the set, name, and color:
```json
"icon": {
"type": "iconify",
"set": "simple-icons",
"name": "python",
"color": "#3776AB"
}
```
**External URLs Rules:**
If an icon is missing from Iconify:
1. Use a **Direct SVG URL** (preferred) or high-res PNG (min 64x64).
2. Must be hosted on a **stable domain** (Wikimedia, GitHub Raw, Official Site).
3. Do not use temporary URLs or hotlink-protected sites.
#### The 3 Main Icon Sets We Use:
1. **Simple Icons** (`"set": "simple-icons"`)
* Used for major brands and single-color logos (like Discord or Python).
* *You must provide a hex `"color"` for these.*
2. **Logos** (`"set": "logos"`)
* Used when an app has a multi-colored, official logo.
* *You don't need a `"color"` for these.*
3. **Material Design** (`"set": "mdi"`)
* Used for generic utilities (like a terminal or a magnifying glass).
* *You must provide a hex `"color"` for these.*
#### Can't find it on Iconify?
If an app's icon isn't on Iconify, you can use a direct link to an image (SVG preferred, or a high-res PNG).
Just change the `"type"` to `"url"`:
```json
"icon": {
"type": "url",
"url": "https://raw.githubusercontent.com/..."
}
```
* ✅ **DO:** Use a stable link (like the app's official GitHub repo raw image, or Wikimedia).
* ❌ **DON'T:** Use temporary image hosts (like Imgur) or hotlink-protected websites.
### 6. Valid Categories

View File

@@ -41,7 +41,8 @@ describe('Data Module', () => {
expect(app.name).toBeTruthy();
expect(app.description).toBeTruthy();
expect(app.category).toBeTruthy();
expect(app.iconUrl).toBeTruthy();
expect(app.icon).toBeDefined();
expect(app.icon.type).toBeTruthy();
expect(app.targets).toBeDefined();
});
});

View File

@@ -2,7 +2,6 @@
import { useState } from 'react';
// App icon with lazy loading, falls back to first letter if it fails
export function AppIcon({ url, name }: { url: string; name: string }) {
const [error, setError] = useState(false);

View File

@@ -2,10 +2,9 @@
import { memo } from 'react';
import { Check } from 'lucide-react';
import { distros, type DistroId, type AppData } from '@/lib/data';
import { distros, getIconUrl, type DistroId, type AppData } from '@/lib/data';
import { isAurPackage } from '@/lib/aur';
import { AppIcon } from './AppIcon';
// Individual app item.
const COLOR_MAP: Record<string, string> = {
'orange': '#f97316',
'blue': '#3b82f6',
@@ -98,7 +97,7 @@ export const AppItem = memo(function AppItem({
>
{isSelected && <Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />}
</div>
<AppIcon url={app.iconUrl} name={app.name} />
<AppIcon url={getIconUrl(app.icon)} name={app.name} />
<div className="flex-1 flex items-baseline gap-1.5 min-w-0 overflow-hidden">
<span
className={`truncate cursor-help ${!isAvailable ? 'text-[var(--text-muted)]' : isSelected ? 'text-[var(--text-primary)]' : 'text-[var(--text-secondary)]'}`}