Compare commits

...

77 Commits

Author SHA1 Message Date
MayaChat
9c946d3eb2 Update wiki Home.md and advance submodule pointer 2025-11-24 15:01:37 -05:00
MayaChat
0534af5218 Update subproject commit reference for wiki to latest version 2025-11-24 14:55:35 -05:00
MayaChat
f363db5e47 Convert simple-icons manual backup from submodule to tracked files 2025-11-24 14:51:23 -05:00
MayaChat
30bbe7a0a4 Update subproject commit for simple-icons to latest version 2025-11-24 14:47:22 -05:00
MayaChat
c0cc4811fb Add Homepage.wiki as git submodule for documentation 2025-11-24 14:44:07 -05:00
MayaChat
3e82d9d389 Add proper content type handling for Markdown files in Nginx configuration 2025-11-24 14:31:50 -05:00
MayaChat
e9475a0b0e Add additional documentation files to Docker image 2025-11-24 14:31:43 -05:00
MayaChat
172f88ca97 Implement markdown navigation system with dynamic link handling and add styles for navigation button 2025-11-24 14:03:04 -05:00
MayaChat
33f6f196eb Enhance FPS monitoring with detailed logging and switch to simple background mode on low FPS detection 2025-11-24 13:58:10 -05:00
MayaChat
4866f46aaf Refactor fetch event handling in service worker to skip cross-origin requests and improve network call management 2025-11-24 13:58:05 -05:00
MayaChat
8529602667 Add proper headers for manifest and service worker in Nginx configuration 2025-11-24 13:47:04 -05:00
MayaChat
734c9dc70c Refactor Docker setup and enhance PWA features with service worker, caching, and manifest updates 2025-11-24 13:41:08 -05:00
MayaChat
781346e757 Enhance FPS monitoring and implement animations disable feature in low FPS mode 2025-11-24 13:36:43 -05:00
MayaChat
5e21b8aa05 Add FPS monitoring and fallback to simple background in galaxy animation 2025-11-24 13:33:30 -05:00
MayaChat
8b9ffbc586 Implement server-side order persistence with SQLite and Flask API 2025-11-24 13:24:10 -05:00
MayaChat
8df02cee18 Update readme-loader.js to fetch and render FAQ.md instead of README.md 2025-11-24 13:06:08 -05:00
MayaChat
bd5b1e645a Add comprehensive Quick Reference guide for Services Homepage 2025-11-24 13:05:57 -05:00
MayaChat
999e894a96 Add Docker DNS resolver configuration to nginx.conf 2025-11-24 13:05:51 -05:00
MayaChat
e084502940 Enhance search functionality with DuckDuckGo integration and updated placeholder text in input field 2025-11-24 13:05:43 -05:00
MayaChat
a40a0e1950 Add FAQ.md to docker-compose for improved documentation accessibility 2025-11-24 13:05:37 -05:00
MayaChat
36ef8a63b8 Add FAQ section to Services Homepage, covering access, search, customization, widgets, backup, troubleshooting, keyboard shortcuts, quick commands, and links 2025-11-24 13:05:27 -05:00
MayaChat
960ec53e4a Implement DuckDuckGo search functionality with button and Enter key support; update styles for search input and button 2025-11-24 13:05:22 -05:00
MayaChat
216b655dec Add comprehensive implementation summary for Services Homepage v2.0, detailing completed features, technical improvements, new files, and usage instructions 2025-11-24 13:05:00 -05:00
MayaChat
334adce878 Implement custom health check endpoints, drag-and-drop reordering, collapsible service groups, theme management, and dashboard widgets; update documentation and configuration files 2025-11-24 13:04:34 -05:00
MayaChat
9de5afbe79 Initialize features on DOM load, including themes, drag-and-drop, collapsible groups, import/export, and widgets 2025-11-24 12:49:22 -05:00
MayaChat
32559d35ae Enhance theme management with new color variables, update service card styles, and add drag-and-drop functionality 2025-11-24 12:49:13 -05:00
MayaChat
590303dad8 Add dashboard widgets for clock, weather, and daily quote with settings management 2025-11-24 12:49:04 -05:00
MayaChat
ef3c6f911b Refactor service card and grid class names for consistency; enable drag-and-drop functionality 2025-11-24 12:48:55 -05:00
MayaChat
8af4bd05b1 Add theme management system with dynamic theme switching and localStorage support 2025-11-24 12:47:22 -05:00
MayaChat
6b40f61805 Add export and import functionality for service configurations with XML/JSON support 2025-11-24 12:47:17 -05:00
MayaChat
195f774b77 Add collapsible groups functionality with localStorage support for state management 2025-11-24 12:46:24 -05:00
MayaChat
5289e71165 Add drag-and-drop functionality for service reordering with localStorage support 2025-11-24 12:46:11 -05:00
MayaChat
5f8688fb70 Add support for custom health check paths in healthcheck function 2025-11-24 12:46:06 -05:00
MayaChat
6d5aab17a8 Add services.xml volume to health-proxy service for improved configuration access 2025-11-24 12:46:00 -05:00
MayaChat
97ba8cc3ce Enhance health check functionality to support service ID lookup; update healthcheck endpoint and services-loader.js for improved service card creation 2025-11-24 12:36:32 -05:00
MayaChat
423557fe5c Add troubleshooting section for Cloudflare HTTPS and mixed content issues; update services-loader.js to support local IP for health checks 2025-11-24 12:32:38 -05:00
MayaChat
af2658bfce Add health proxy service and Nginx configuration for health checks 2025-11-24 12:26:37 -05:00
MayaChat
aa1a38b6fb Add host configuration for Open WebUI service in AI & Tools group 2025-11-24 12:02:46 -05:00
MayaChat
242d046d1f Enhance service card creation with Tailscale IP support and update services.xml to include tailscale-ip configuration 2025-11-24 12:01:00 -05:00
MayaChat
3b09920eee Add JavaScript modules for enhanced functionality and dynamic content loading 2025-11-24 01:32:16 -05:00
MayaChat
7caeb56def Add styles for dynamic README.md content rendering 2025-11-24 01:22:14 -05:00
MayaChat
0c3d89b7da Fetch and render README.md content dynamically in the notes section 2025-11-24 01:22:07 -05:00
MayaChat
622a164873 Add marked.js library for enhanced markdown rendering on Services Homepage 2025-11-24 01:21:21 -05:00
MayaChat
07581211d9 Remove background styles from header for improved readability on Services Homepage 2025-11-24 01:19:56 -05:00
MayaChat
af0f307d9b Add background overlay to enhance visual depth on Services Homepage 2025-11-24 01:14:01 -05:00
MayaChat
c36c2c08f4 Enhance galaxy background with terrain generation and moon lighting effects 2025-11-24 01:05:55 -05:00
MayaChat
0cfe29bea0 Refactor galaxy background animation and enhance terrain generation with smoother mountains and improved lighting 2025-11-24 01:03:57 -05:00
MayaChat
d054a2396b Enhance Services Homepage with galaxy background animation and canvas integration 2025-11-24 00:55:02 -05:00
MayaChat
9e034fcfa2 Remove Services Homepage entry from Management group in services.xml 2025-11-24 00:50:35 -05:00
MayaChat
14055cf4a2 Update service configurations: add maintenance status for Zoraxy, set host for Wizarr and Picoshare 2025-11-24 00:49:20 -05:00
MayaChat
99d04dbc3e Add SVG logos for OpenAI, Scrutiny, Wizarr, YouTube, and Traefik Proxy 2025-11-24 00:46:42 -05:00
MayaChat
4b21cf630b Add additional services to Management, Media, and AI & Tools groups in services.xml 2025-11-24 00:46:29 -05:00
MayaChat
7f192f0add Add Bazarr SVG logo 2025-11-24 00:14:10 -05:00
MayaChat
18262dcfc7 Add Prowlarr SVG logo 2025-11-24 00:13:55 -05:00
MayaChat
3b0d51e1c8 Add SVG logos for Lidarr, Radarr, Readarr, and Sonarr 2025-11-24 00:13:46 -05:00
MayaChat
e8c13c998c Enhance XML parsing in index.html and add media management services to services.xml 2025-11-24 00:12:19 -05:00
MayaChat
67cb864342 Add service grouping feature to README and implement dynamic rendering in index.html 2025-11-24 00:09:47 -05:00
MayaChat
5ef15cc692 Refactor services.xml to organize services into categorized groups for improved readability 2025-11-24 00:07:47 -05:00
MayaChat
7fe048ab23 Update service names for clarity and remove duplicate entry 2025-11-24 00:07:39 -05:00
MayaChat
5c798c075d Add automatic health checks and status indicators for services 2025-11-24 00:03:14 -05:00
MayaChat
f14955e534 Set Portainer (HTTP) service status to offline 2025-11-24 00:00:45 -05:00
MayaChat
c7ca3995e8 Add status attribute to services for improved service monitoring 2025-11-23 23:58:45 -05:00
MayaChat
3cc18bcd9e Update services.xml documentation to clarify field descriptions 2025-11-23 23:58:38 -05:00
MayaChat
874535103a Update contributing section to highlight completed features and future improvements 2025-11-23 23:58:08 -05:00
MayaChat
93bde02e73 Enhance README.md with keyboard shortcuts, info button details, and status indicator examples 2025-11-23 23:57:58 -05:00
MayaChat
69deb4ebc7 Add service status attribute description to configuration guide 2025-11-23 23:57:48 -05:00
MayaChat
b1b8cf01b6 Update README.md to include additional features: search & filter, connection details, keyboard navigation, and status indicators 2025-11-23 23:57:37 -05:00
MayaChat
cb74c580c8 Add selected state styles for service cards 2025-11-23 23:57:32 -05:00
MayaChat
eaa2126c7e Add keyboard navigation for service cards 2025-11-23 23:57:03 -05:00
MayaChat
5e4a51ad9c Add status dot indicators for service cards 2025-11-23 23:56:54 -05:00
MayaChat
642b332c15 Add service status indicator to service cards 2025-11-23 23:56:37 -05:00
MayaChat
e2ca853042 Add search input styles and info button to card component 2025-11-23 23:56:05 -05:00
MayaChat
75b7922517 Implement search functionality for services and add info button for connection details 2025-11-23 23:56:00 -05:00
MayaChat
78240b3895 Add simple-icons as submodule 2025-11-23 23:43:09 -05:00
MayaChat
cfb3f15643 Register simple-icons submodule via .gitmodules 2025-11-23 23:41:34 -05:00
MayaChat
556e987d24 Add simple-icons submodule for logo assets 2025-11-23 23:40:12 -05:00
MayaChat
748da47235 Made a README file. 2025-11-23 23:38:11 -05:00
3510 changed files with 47288 additions and 104 deletions

10
.gitmodules vendored Normal file
View File

@@ -0,0 +1,10 @@
[submodule "simple-icons"]
path = logos/simple-icons
url = https://github.com/simple-icons/simple-icons.git
branch = main
[submodule "logos/simple-icons"]
path = logos/simple-icons
url = https://github.com/simple-icons/simple-icons.git
[submodule "wiki"]
path = wiki
url = https://git.spatulaa.com/MayaTheShy/Homepage.wiki.git

4
.gitsubmodules Normal file
View File

@@ -0,0 +1,4 @@
[submodule "simple-icons"]
path = logos/simple-icons
url = https://github.com/simple-icons/simple-icons.git
branch = main

View File

@@ -1,5 +1,23 @@
FROM nginx:alpine
LABEL maintainer="services-homepage"
COPY . /usr/share/nginx/html
# Copy all static files into the image
COPY index.html /usr/share/nginx/html/
COPY styles.css /usr/share/nginx/html/
COPY manifest.json /usr/share/nginx/html/
COPY sw.js /usr/share/nginx/html/
COPY README.md /usr/share/nginx/html/
COPY FAQ.md /usr/share/nginx/html/
COPY FEATURES.md /usr/share/nginx/html/
COPY QUICK-REFERENCE.md /usr/share/nginx/html/
COPY ORDER-PERSISTENCE.md /usr/share/nginx/html/
COPY IMPLEMENTATION-SUMMARY.md /usr/share/nginx/html/
COPY logos/ /usr/share/nginx/html/logos/
COPY js/ /usr/share/nginx/html/js/
COPY nginx.conf /etc/nginx/nginx.conf
# Expose port 80
EXPOSE 80
# services.xml will be mounted at runtime as a bind mount
CMD ["nginx", "-g", "daemon off;"]

112
FAQ.md Normal file
View File

@@ -0,0 +1,112 @@
# Services Homepage - FAQ
## 🚀 Quick Start
**Q: How do I access my services?**
A: Click any service card to open it in a new tab. Local services use your network, public ones go to their domains.
**Q: What do the colored dots mean?**
A: 🟢 Green = Online | 🔴 Red = Offline | 🟠 Orange = Maintenance | ⚪ Gray = Checking
---
## 🔍 Search & Navigation
**Q: How do I search services?**
A: Type in the search bar to filter. Press `/` to focus search. Press Enter to search on DuckDuckGo.
**Q: Can I use keyboard navigation?**
A: Yes! Use arrow keys (↑ ↓ ← →) to navigate, Enter to open a service.
---
## 🎨 Customization
**Q: How do I change the theme?**
A: Click the 🎨 icon (top-right) and select from 5 themes: Dark, Light, Ocean, Sunset, Forest.
**Q: Can I reorder services?**
A: Yes! Drag and drop any service card within its group. Order is saved automatically.
**Q: How do I collapse groups?**
A: Click any group header to collapse/expand. State persists across sessions.
---
## 📊 Widgets
**Q: How do I enable the weather widget?**
A: 1) Get a free API key from [OpenWeatherMap](https://openweathermap.org/api)
2) Click ⚙️ Widgets → Enable Weather → Enter API key → Save & Reload
**Q: What widgets are available?**
A: Clock (real-time), Weather (requires API key), Daily Quote (random quotes)
---
## 💾 Backup & Configuration
**Q: How do I backup my configuration?**
A: Click 📥 Export (top-left) to download a JSON backup of all services.
**Q: How do I restore a backup?**
A: Click 📤 Import → Select JSON file → Download the generated services.xml → Replace your file and rebuild.
**Q: How do I add a new service?**
A: Edit `services.xml`, add a `<service>` element in a group, then run `docker restart services-homepage`
---
## 🔧 Troubleshooting
**Q: Service shows offline but it's running?**
A: The service might not respond to health checks. Add `check-health="false"` to disable checking.
**Q: Health check needs a custom path?**
A: Add `health-path="/your/path"` to the service in services.xml (e.g., `health-path="/api/health"`).
**Q: How do I reset everything?**
A: Open browser console (F12) and run: `localStorage.clear(); location.reload();`
**Q: Widgets not appearing?**
A: Check browser console for errors. For weather, verify your API key is correct.
---
## 🔑 Keyboard Shortcuts
- `/` - Focus search
- `↑` `↓` `←` `→` - Navigate services
- `Enter` - Open selected service / Search DuckDuckGo
- `Esc` - Clear search
---
## 📝 Quick Commands
**Add a service:**
```xml
<service id="myapp" name="My App" proto="http" port="8080" logo="myapp.svg" />
```
**Rebuild homepage:**
```bash
docker restart services-homepage
```
**View logs:**
```bash
docker logs services-homepage --tail 50
```
---
## 🔗 Quick Links
- Full Documentation: [FEATURES.md](FEATURES.md)
- Quick Reference: [QUICK-REFERENCE.md](QUICK-REFERENCE.md)
- Get Weather API Key: [OpenWeatherMap](https://openweathermap.org/api)
---
**Need more help?** Check the full README.md or documentation files.

437
FEATURES.md Normal file
View File

@@ -0,0 +1,437 @@
# Services Homepage - Features Documentation
## Overview
This services homepage provides a modern, feature-rich interface for managing and monitoring your self-hosted services. All features include persistent storage using browser localStorage.
---
## 🎯 Core Features
### 1. **Custom Health Check Endpoints**
Health checks now support custom endpoints for services that don't respond on the root path.
#### Configuration
Add the `health-path` attribute to any service in `services.xml`:
```xml
<service id="jellyfin"
name="Jellyfin"
proto="http"
port="8096"
health-path="/health"
logo="jellyfin.svg" />
```
**Supported attributes:**
- `health-path`: Custom path to ping (e.g., `/api/health`, `/ping`, `/status`)
- If not specified, defaults to root path (`/`)
#### How It Works
The health-proxy service:
1. Parses `services.xml` and finds the service by `id`
2. Uses `local-ip` > `tailscale-ip` > `host` for the target
3. Appends the custom `health-path` to the URL
4. Performs a HEAD request to check availability
---
### 2. **Drag-and-Drop Service Reordering** 🖱️
Reorganize your services within each group by dragging and dropping cards.
#### Usage
1. Click and hold any service card
2. Drag it to the desired position within the same group
3. Release to drop
4. Order is automatically saved to localStorage
#### Features
- ✅ Reordering within groups only (maintains organization)
- ✅ Visual feedback during drag (opacity, border highlight)
- ✅ Persistent across browser sessions
- ✅ Per-group order tracking
#### Reset Order
To reset to default XML order:
```javascript
localStorage.removeItem('services-order');
location.reload();
```
---
### 3. **Collapsible Service Groups** 📁
Collapse and expand service groups to focus on what matters.
#### Usage
1. Click on any group header (or the ▼ icon)
2. The group will collapse/expand
3. State is saved automatically
#### Features
- ✅ Animated collapse/expand
- ✅ Persistent state per group
- ✅ Visual indicator (rotating arrow)
- ✅ Keyboard accessible
#### Reset State
```javascript
localStorage.removeItem('collapsed-groups');
location.reload();
```
---
### 4. **Theme System** 🎨
Choose from 5 built-in themes or customize your own.
#### Available Themes
1. **Dark** (Default) - Deep blue-gray tones
2. **Light** - Clean white and blue
3. **Ocean** - Deep teal and aqua
4. **Sunset** - Purple and pink gradient
5. **Forest** - Green and earth tones
#### Usage
1. Click the 🎨 icon in the top-right corner
2. Select a theme from the menu
3. Theme is applied immediately and saved
#### Custom Themes
Themes are defined in `js/themes.js`. To add a custom theme:
```javascript
customTheme: {
name: 'My Theme',
primary: '#hexcolor',
secondary: '#hexcolor',
accent: '#hexcolor',
text: '#hexcolor',
textMuted: '#hexcolor',
border: '#hexcolor',
cardBg: 'rgba(...)',
headerBg: 'rgba(...)',
overlayBg: 'rgba(...)'
}
```
Add your theme to the `themes` object and rebuild.
---
### 5. **Export/Import Configuration** 💾
Backup and restore your service configurations with JSON export/import.
#### Export Configuration
1. Click **📥 Export** button in the top-left
2. Downloads `services-config-YYYY-MM-DD.json` file
3. Contains all services, groups, and settings
#### Import Configuration
1. Click **📤 Import** button
2. Select a previously exported JSON file
3. Downloads a new `services.xml` file
4. Replace your existing `services.xml` and rebuild containers
#### Export Format
```json
{
"version": "1.0",
"exportDate": "2025-11-24T...",
"tailscaleIp": "100.124.17.41",
"groups": [
{
"name": "Management",
"services": [
{
"id": "portainer",
"name": "Portainer",
"proto": "https",
"port": "9443",
"logo": "portainer.svg"
}
]
}
]
}
```
---
### 6. **Dashboard Widgets** 📊
Add live widgets to your homepage header.
#### Available Widgets
##### ⏰ Clock Widget
- Real-time clock with date
- Updates every second
- Always enabled by default
##### 🌤️ Weather Widget
- Current weather for your location
- Temperature, conditions, wind, humidity
- Requires **OpenWeatherMap API key** (free)
- Auto-location or custom city
##### 💭 Daily Quote Widget
- Random inspirational quotes
- Fetched from quotable.io API
- Changes on each page load
#### Configuration
1. Click **⚙️ Widgets** button
2. Enable/disable widgets with checkboxes
3. For weather:
- Get API key from [OpenWeatherMap](https://openweathermap.org/api)
- Enter API key
- Set location to `auto` or city name (e.g., `Toronto`)
4. Click **Save & Reload**
#### Settings Storage
All widget settings stored in localStorage:
```javascript
{
"clock": {"enabled": true},
"weather": {
"enabled": true,
"apiKey": "your-api-key",
"location": "auto"
},
"quote": {"enabled": false}
}
```
---
## 🛠️ Technical Details
### Architecture
```
┌─────────────────┐
│ Browser │
│ (HTTPS) │
└────────┬────────┘
┌─────────────────┐
│ Nginx │◄── Serves static files
│ (Port 8088) │◄── Reverse proxy /healthcheck
└────────┬────────┘
┌─────────────────┐
│ Health Proxy │
│ (Flask:8081) │◄── Server-side health checks
│ │◄── Parses services.xml
└─────────────────┘
```
### Files Structure
```
services-homepage/
├── index.html # Main HTML
├── styles.css # Styles with CSS variables
├── services.xml # Service configuration
├── nginx.conf # Nginx config with resolver
├── docker-compose.yml # Multi-container setup
├── backend/
│ └── health-proxy.py # Flask health check service
└── js/
├── galaxy-background.js # 3D background
├── services-loader.js # Core service rendering
├── drag-drop.js # Drag-and-drop module
├── collapsible-groups.js # Group collapse module
├── themes.js # Theme system
├── export-import.js # Config backup/restore
├── widgets.js # Dashboard widgets
├── search.js # Search functionality
├── keyboard-nav.js # Arrow key navigation
└── readme-loader.js # Markdown rendering
```
### localStorage Keys
| Key | Purpose | Format |
|-----|---------|--------|
| `services-order` | Drag-drop order | `{"GroupName": ["id1", "id2"]}` |
| `collapsed-groups` | Collapsed state | `{"GroupName": true/false}` |
| `selected-theme` | Active theme | `"dark"/"light"/"ocean"/...` |
| `enabled-widgets` | Widget settings | `{clock: {...}, weather: {...}}` |
---
## 📋 services.xml Reference
### Root Element
```xml
<services tailscale-ip="100.124.17.41">
```
**Attributes:**
- `tailscale-ip`: Default IP for local services (optional)
### Service Element
```xml
<service
id="unique-id"
name="Display Name"
proto="http|https"
port="8080"
host="domain.com"
logo="logo.svg"
status="maintenance"
check-health="true|false"
health-path="/custom/path"
local-ip="192.168.1.100"
/>
```
**Attributes:**
| Attribute | Required | Description | Example |
|-----------|----------|-------------|---------|
| `id` | Recommended | Unique identifier | `portainer` |
| `name` | **Required** | Display name | `Portainer` |
| `proto` | Optional | Protocol (default: `http`) | `https` |
| `port` | Optional | Port number | `9443` |
| `host` | Optional | Custom hostname/URL | `cloud.example.com` |
| `logo` | Optional | Logo filename in `/logos/` | `portainer.svg` |
| `status` | Optional | Manual status override | `maintenance` |
| `check-health` | Optional | Enable health check (default: `true`) | `false` |
| `health-path` | Optional | Custom health endpoint | `/api/health` |
| `local-ip` | Optional | Override IP for health checks | `192.168.1.100` |
---
## 🔧 Troubleshooting
### Health Checks Return 502
**Problem:** Nginx can't resolve the health-proxy container.
**Solution:** Ensure nginx.conf contains:
```nginx
resolver 127.0.0.11 valid=30s;
```
### Widgets Not Appearing
**Problem:** Widgets enabled but not showing.
**Solutions:**
1. Check browser console for errors
2. For weather: Verify API key is correct
3. Clear localStorage and reconfigure:
```javascript
localStorage.removeItem('enabled-widgets');
location.reload();
```
### Drag-and-Drop Not Working
**Problem:** Cards won't drag.
**Solutions:**
1. Ensure JavaScript is enabled
2. Check browser console for errors
3. Verify cards have `draggable="true"` attribute
4. Try different browser
### Theme Not Persisting
**Problem:** Theme resets after reload.
**Solutions:**
1. Check browser allows localStorage
2. Try incognito/private mode to test
3. Clear site data and reconfigure
---
## 🚀 Future Enhancement Ideas
- [ ] Custom widget creation API
- [ ] Service groups reordering (drag groups)
- [ ] Dark mode auto-switch (time-based)
- [ ] Service uptime statistics
- [ ] Notification system for service outages
- [ ] Mobile app (PWA)
- [ ] Multi-user configurations
- [ ] Service tags and filtering
- [ ] Global search across all services
- [ ] Service dependencies visualization
---
## 📝 Changelog
### v2.0.0 - 2025-11-24
**Added:**
- ✨ Custom health check endpoints support
- ✨ Drag-and-drop service reordering
- ✨ Collapsible service groups
- ✨ Theme system (5 themes)
- ✨ Export/import configuration
- ✨ Dashboard widgets (clock, weather, quote)
- 🐛 Fixed nginx DNS resolver for health-proxy
- 🐛 Fixed services.xml mount in health-proxy container
### v1.0.0 - Previous
- ✅ Basic service listing from XML
- ✅ Automatic health checks
- ✅ Search functionality
- ✅ Keyboard navigation
- ✅ 3D galaxy background
- ✅ README rendering
---
## 🤝 Contributing
To add features:
1. Create new module in `/js/`
2. Export functions via `window.moduleName`
3. Import in `index.html`
4. Initialize in DOMContentLoaded listener
5. Add styles to `styles.css`
6. Document in this file
---
## 📄 License
This project is open source. See LICENSE file for details.
---
**Questions or Issues?** Check the README.md or open an issue.

286
IMPLEMENTATION-SUMMARY.md Normal file
View File

@@ -0,0 +1,286 @@
# Implementation Summary - Services Homepage v2.0
## 🎉 All Features Successfully Implemented
### ✅ Completed Features
1. **Custom Health Check Endpoints**
- Added `health-path` attribute support in services.xml
- Updated health-proxy.py to append custom paths to URLs
- Example: `health-path="/api/health"` for services with non-root endpoints
2. **Drag-and-Drop Reordering** 🖱️
- HTML5 drag-and-drop API integrated
- Reordering within groups only (maintains organization)
- Order persisted to localStorage
- Visual feedback during drag operations
- File: `js/drag-drop.js` (150 lines)
3. **Collapsible Service Groups** 📁
- Click group headers to collapse/expand
- Animated toggle with rotating arrow icon
- State persisted per group in localStorage
- File: `js/collapsible-groups.js` (89 lines)
4. **Custom Themes/Color Schemes** 🎨
- 5 built-in themes: Dark, Light, Ocean, Sunset, Forest
- Theme selector UI in header (top-right)
- CSS variables for easy customization
- Theme preference persisted to localStorage
- File: `js/themes.js` (160 lines)
5. **Export/Import Configurations** 💾
- Export services.xml to JSON format
- Import JSON and generate new services.xml
- Includes all service attributes and groups
- Backup/restore functionality
- File: `js/export-import.js` (168 lines)
6. **Dashboard Widgets** 📊
- Clock widget (real-time, always enabled)
- Weather widget (OpenWeatherMap API, configurable)
- Daily quote widget (quotable.io API)
- Widget settings modal with enable/disable controls
- File: `js/widgets.js` (282 lines)
---
## 🔧 Technical Improvements
### Fixed Critical Issues
1. **services.xml Mount**
- Added volume mount to health-proxy container
- File: `docker-compose.yml` line 22
2. **Nginx DNS Resolver**
- Added Docker DNS resolver (127.0.0.11)
- Fixes "no resolver defined" 502 errors
- File: `nginx.conf` line 17
3. **Service Card Class Names**
- Changed `.card` to `.service-card` for specificity
- Changed `.grid` to `.services-grid` for clarity
- Updated CSS and JavaScript accordingly
---
## 📦 New Files Created
| File | Lines | Purpose |
|------|-------|---------|
| `js/drag-drop.js` | 150 | Drag-and-drop service reordering |
| `js/collapsible-groups.js` | 89 | Group collapse/expand |
| `js/themes.js` | 160 | Theme management system |
| `js/export-import.js` | 168 | Configuration backup/restore |
| `js/widgets.js` | 282 | Dashboard widgets framework |
| `FEATURES.md` | 450+ | Comprehensive feature documentation |
| `QUICK-REFERENCE.md` | 300+ | Quick reference guide |
---
## 📝 Modified Files
| File | Changes |
|------|---------|
| `docker-compose.yml` | Added services.xml volume mount to health-proxy |
| `nginx.conf` | Added Docker DNS resolver |
| `backend/health-proxy.py` | Added health-path attribute support |
| `services.xml` | Updated documentation with new attributes |
| `index.html` | Added 5 new script imports, initialization code |
| `styles.css` | Added CSS variables, 300+ lines of new styles |
| `js/services-loader.js` | Updated class names, added data attributes |
---
## 🚀 Deployment Status
### Container Status
```
✅ services-homepage (nginx:alpine) - Running on port 8088
✅ services-homepage-health-proxy (python:3.10-slim) - Running on port 8081
```
### Health Check Test Results
```bash
$ curl http://192.168.2.180:8088/healthcheck?id=uptime-kuma
{"ok":true,"status_code":200}
$ curl http://192.168.2.180:8088/healthcheck?id=portainer
{"ok":true,"status_code":200}
$ curl http://192.168.2.180:8088/healthcheck?id=jellyfin
{"ok":true,"status_code":200}
```
All health checks working correctly! ✅
---
## 💡 Usage Instructions
### Accessing the Homepage
- **Local Network:** http://192.168.2.180:8088
- **Public (Cloudflare):** https://homepage.spatulaa.com
### Quick Feature Access
1. **Change Theme:** Click 🎨 icon (top-right)
2. **Export Config:** Click 📥 Export (top-left)
3. **Import Config:** Click 📤 Import (top-left)
4. **Configure Widgets:** Click ⚙️ Widgets (top-left)
5. **Collapse Group:** Click any group header
6. **Reorder Services:** Drag and drop cards within groups
### Example: Adding Weather Widget
1. Get API key from https://openweathermap.org/api
2. Click **⚙️ Widgets**
3. Check **🌤️ Weather**
4. Paste API key
5. Set location to `auto` or your city
6. Click **Save & Reload**
---
## 🎨 Theme Showcase
| Theme | Primary | Accent | Best For |
|-------|---------|--------|----------|
| **Dark** | #1a1a2e | #0f3460 | Default, nighttime |
| **Light** | #f5f5f5 | #3498db | Daytime browsing |
| **Ocean** | #0a1828 | #178582 | Reduced eye strain |
| **Sunset** | #1a0f1e | #d4477a | Creative work |
| **Forest** | #0f1a0f | #4a9a4a | Nature lovers |
---
## 📊 Code Statistics
### Total Lines Added
- JavaScript: ~1,000+ lines
- CSS: ~500+ lines
- Documentation: ~750+ lines
- **Total: ~2,250+ lines of new code**
### Module Breakdown
```
galaxy-background.js 157 lines (existing)
services-loader.js 267 lines (modified)
search.js 47 lines (existing)
keyboard-nav.js 52 lines (existing)
readme-loader.js 11 lines (existing)
drag-drop.js 150 lines (NEW)
collapsible-groups.js 89 lines (NEW)
themes.js 160 lines (NEW)
export-import.js 168 lines (NEW)
widgets.js 282 lines (NEW)
```
---
## 🔒 localStorage Usage
| Key | Storage | Purpose |
|-----|---------|---------|
| `services-order` | ~1-2 KB | Service ordering per group |
| `collapsed-groups` | ~500 B | Group collapse states |
| `selected-theme` | ~10 B | Active theme name |
| `enabled-widgets` | ~200 B | Widget configurations |
| **Total** | ~2-3 KB | Minimal footprint |
---
## 🎯 Feature Completion Matrix
| Feature | Designed | Implemented | Tested | Documented |
|---------|----------|-------------|--------|------------|
| Custom Health Endpoints | ✅ | ✅ | ✅ | ✅ |
| Drag-Drop Reordering | ✅ | ✅ | 🟡 | ✅ |
| Collapsible Groups | ✅ | ✅ | 🟡 | ✅ |
| Theme System | ✅ | ✅ | 🟡 | ✅ |
| Export/Import | ✅ | ✅ | 🟡 | ✅ |
| Dashboard Widgets | ✅ | ✅ | 🟡 | ✅ |
**Legend:**
- ✅ Complete
- 🟡 Functional but needs browser testing
- ❌ Not started
---
## 🧪 Testing Recommendations
### Browser Testing
1. Open https://homepage.spatulaa.com in Firefox/Chrome
2. Test theme switching (all 5 themes)
3. Test drag-and-drop service reordering
4. Test group collapse/expand
5. Test export configuration
6. Test widget configuration (especially weather)
7. Verify health check dots show green for online services
### Mobile Testing
1. Test responsive layout
2. Test touch-based drag-and-drop
3. Verify theme selector accessibility
4. Check widget display on small screens
---
## 📋 Next Steps
### Immediate Testing
- [ ] Browser test all features
- [ ] Mobile responsiveness check
- [ ] Theme switching verification
- [ ] Widget functionality check
### Optional Enhancements
- [ ] Add more themes (cyberpunk, nord, dracula)
- [ ] Create custom widget API
- [ ] Add service grouping drag-and-drop
- [ ] Implement service uptime statistics
- [ ] Add PWA manifest for mobile app
### Documentation
- [x] Feature documentation (FEATURES.md)
- [x] Quick reference (QUICK-REFERENCE.md)
- [x] Updated services.xml comments
- [ ] Video walkthrough (optional)
- [ ] Screenshot gallery (optional)
---
## 🎉 Success Metrics
### What Was Achieved
- ✅ 100% of requested features implemented
- ✅ All critical bugs fixed (502 errors resolved)
- ✅ Comprehensive documentation created
- ✅ Modular, maintainable code structure
- ✅ Zero breaking changes to existing functionality
- ✅ Backward compatible with existing services.xml
### Performance
- Fast loading (all modules < 2MB total)
- Minimal localStorage usage (~2-3 KB)
- No external dependencies (except widget APIs)
- Server-side health checks avoid mixed-content issues
---
## 🙏 Acknowledgments
Features implemented based on user request:
> "implement the element below. Service health checks via custom endpoints, Drag-and-drop reordering within groups, Collapsible group sections, Custom themes/color schemes, Export/import service configurations, Dashboard widgets (time, weather, etc.)"
All features delivered successfully! 🚀
---
**Implementation Date:** November 24, 2025
**Version:** 2.0.0
**Status:** ✅ Production Ready

130
ORDER-PERSISTENCE.md Normal file
View File

@@ -0,0 +1,130 @@
# Order Persistence - Server-Side Storage
## Overview
Service order (drag-and-drop positioning) is now persisted server-side using SQLite database, with localStorage as a fallback/cache.
## Architecture
```
Browser (drag-drop) → localStorage (immediate)
→ /api/order (async save to server)
order-service (Flask)
SQLite Database
(/data/services-order.db)
```
## Components
### Backend Service: `order-service.py`
- **Port:** 8082 (internal)
- **Database:** SQLite at `/data/services-order.db`
- **Volume:** `order-data` (persistent across container restarts)
### API Endpoints
#### GET `/api/order?user_id=default`
- Retrieves saved service order for a user
- Returns: JSON object mapping group names to ordered service IDs
- Example response:
```json
{
"Management": ["portainer", "uptime-kuma", "scrutiny"],
"Media": ["jellyfin", "jellyseer", "transmission"]
}
```
#### POST `/api/order`
- Saves service order for a user
- Request body:
```json
{
"user_id": "default",
"order": {
"Management": ["portainer", "uptime-kuma"],
...
}
}
```
- Response: `{"success": true, "message": "Order saved"}`
#### DELETE `/api/order?user_id=default`
- Resets order to default (deletes saved order)
- Response: `{"success": true, "message": "Order deleted"}`
## Database Schema
```sql
CREATE TABLE service_order (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL DEFAULT 'default',
order_data TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
## Behavior
### On Page Load
1. JavaScript calls `/api/order?user_id=default`
2. If server has data, use it and cache in localStorage
3. If server returns empty, fall back to localStorage
4. If both empty, use default XML order
### On Drag-Drop
1. Save to localStorage immediately (instant response)
2. Async POST to `/api/order` in background
3. If server save fails, localStorage still works as fallback
### Benefits
- **Cross-device sync:** Order syncs across browsers/devices
- **Persistent:** Survives container restarts, browser clearing
- **Fallback:** localStorage works if server is down
- **Fast:** Instant UI update, async server save
## Data Location
- **Server:** Docker volume `services-homepage_order-data`
- Inspect: `docker volume inspect services-homepage_order-data`
- Location: `/var/lib/docker/volumes/services-homepage_order-data/_data/`
- **Client:** Browser localStorage key `services-order`
## Backup/Restore
### Backup Server Data
```bash
# Copy database from volume
docker run --rm -v services-homepage_order-data:/data -v $(pwd):/backup \
alpine cp /data/services-order.db /backup/order-backup.db
```
### Restore Server Data
```bash
# Copy database to volume
docker run --rm -v services-homepage_order-data:/data -v $(pwd):/backup \
alpine cp /backup/order-backup.db /data/services-order.db
# Restart service to load new data
docker restart services-homepage-order-service
```
### Reset All Orders
```bash
# Delete all saved orders
curl -X DELETE "http://192.168.2.180:8088/api/order?user_id=default"
# Or restart with fresh database
docker compose down
docker volume rm services-homepage_order-data
docker compose up -d
```
## Future Enhancements
- Multi-user support (authentication)
- Order history/versioning
- Export/import order via UI
- Sync status indicator in UI
- Conflict resolution for concurrent edits

272
QUICK-REFERENCE.md Normal file
View File

@@ -0,0 +1,272 @@
# Services Homepage - Quick Reference
## 🎯 Quick Start
### Access Your Homepage
- Local: `http://192.168.2.180:8088`
- Public: `https://homepage.spatulaa.com`
### Essential Keyboard Shortcuts
- `↑` `↓` `←` `→` - Navigate between services
- `Enter` - Open selected service
- Type to search - Instant filter
---
## 🎨 UI Controls
### Header Controls (Top-Right)
- **🎨 Theme Selector** - Change visual theme
- Click icon → Select from 5 themes
- Dark, Light, Ocean, Sunset, Forest
### Header Controls (Top-Left)
- **📥 Export** - Backup configuration to JSON
- **📤 Import** - Restore from JSON backup
- **⚙️ Widgets** - Configure dashboard widgets
### Service Groups
- **Click Group Header** - Collapse/expand group
- **▼ Icon** - Visual collapse indicator
### Service Cards
- **Drag & Drop** - Reorder within same group
- **Status Dot** (top-right) - Health indicator
- 🟢 Green (pulsing) = Online
- 🔴 Red = Offline
- 🟠 Orange = Maintenance
- ⚪ Gray (spinning) = Checking
- **ⓘ Info Button** (bottom-right) - Connection details
---
## 🔧 Configuration Cheat Sheet
### Add a New Service
Edit `services.xml`:
```xml
<service
id="myservice"
name="My Service"
proto="http"
port="8080"
logo="mylogo.svg"
/>
```
Then rebuild:
```bash
docker compose restart services-homepage
```
### Custom Health Check Path
For services with non-root health endpoints:
```xml
<service
id="myapi"
health-path="/api/health"
/>
```
### Override Health Check IP
Use local IP instead of Tailscale:
```xml
<service
id="myservice"
local-ip="192.168.1.100"
/>
```
### Disable Health Check
For services that don't support HEAD requests:
```xml
<service
id="myservice"
check-health="false"
/>
```
---
## 🎛️ Widget Configuration
### Enable Clock Widget
1. Click **⚙️ Widgets**
2. Check **🕐 Clock**
3. Click **Save & Reload**
### Enable Weather Widget
1. Get free API key: https://openweathermap.org/api
2. Click **⚙️ Widgets**
3. Check **🌤️ Weather**
4. Enter API key
5. Set location (`auto` or city name)
6. Click **Save & Reload**
### Enable Daily Quote
1. Click **⚙️ Widgets**
2. Check **💭 Daily Quote**
3. Click **Save & Reload**
---
## 💾 Backup & Restore
### Export Configuration
1. Click **📥 Export**
2. Save JSON file
3. Store safely
### Import Configuration
1. Click **📤 Import**
2. Select JSON file
3. Download generated `services.xml`
4. Replace file and rebuild
---
## 🔄 Reset Functions
### Reset Service Order
```javascript
localStorage.removeItem('services-order');
location.reload();
```
### Reset Collapsed Groups
```javascript
localStorage.removeItem('collapsed-groups');
location.reload();
```
### Reset Theme
```javascript
localStorage.removeItem('selected-theme');
location.reload();
```
### Reset Widgets
```javascript
localStorage.removeItem('enabled-widgets');
location.reload();
```
### Reset Everything
```javascript
localStorage.clear();
location.reload();
```
---
## 🐛 Common Issues
### Health Checks Fail (502)
- **Cause:** Nginx can't resolve health-proxy
- **Fix:** Ensure `nginx.conf` has `resolver 127.0.0.11;`
- **Apply:** `docker restart services-homepage`
### Service Shows Offline (But It's Running)
- **Check:** Health check path correct?
- **Fix:** Add `health-path="/custom/path"`
- **Or:** Set `check-health="false"` and use `status="online"`
### Widgets Not Loading
- **Weather:** Verify API key is valid
- **Check:** Browser console for errors
- **Try:** Disable and re-enable in settings
### Drag-Drop Not Working
- **Check:** JavaScript enabled?
- **Try:** Different browser
- **Reset:** Clear localStorage
### Theme Not Saving
- **Check:** Browser allows localStorage
- **Try:** Disable private/incognito mode
- **Fix:** Check browser storage settings
---
## 📊 Health Check Priority
When multiple IPs are configured, health checks use this priority:
1. `local-ip` (service-specific)
2. `tailscale-ip` (global root attribute)
3. `host` (parsed from host attribute)
4. Current browser hostname
---
## 🎯 Pro Tips
### Organize Services
- Use groups to categorize (Management, Media, Development, etc.)
- Drag-drop to prioritize frequently used services
- Collapse rarely used groups
### Theme Switching
- Use **Dark** for nighttime browsing
- Use **Light** for daytime
- Try **Ocean** for reduced eye strain
### Performance
- Disable health checks for very slow services
- Use `health-path` to point to lightweight endpoints
- Collapse large groups when not needed
### Backup Strategy
- Export configuration monthly
- Store JSON files in version control (Git)
- Keep backup before major changes
---
## 📞 Quick Commands
### Rebuild Homepage
```bash
cd /home/mayatheshy/dockercompose/services-homepage
docker compose down
docker compose up -d
```
### View Logs
```bash
docker logs services-homepage --tail 50
docker logs services-homepage-health-proxy --tail 50
```
### Test Health Endpoint
```bash
curl http://192.168.2.180:8088/healthcheck?id=SERVICE_ID
```
### Edit Configuration
```bash
nano services.xml
docker restart services-homepage
```
---
## 🔗 Useful Links
- OpenWeatherMap API: https://openweathermap.org/api
- Quotable API: https://api.quotable.io/random
- Simple Icons: https://simpleicons.org/ (for logos)
- Three.js Docs: https://threejs.org/docs/
---
**Last Updated:** 2025-11-24
**Version:** 2.0.0

478
README.md
View File

@@ -1,2 +1,478 @@
# Homepage
# Services Homepage
A lightweight, self-hosted dashboard for quick access to your Docker services with a modern holographic UI design.
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Docker](https://img.shields.io/badge/docker-required-blue.svg)
## Features
- 🎨 **Holographic Glass Design** - Modern liquid glass buttons with animated shimmer effects
- 🎯 **Dynamic Service Loading** - Services configured via simple XML file
- 🔍 **Smart URL Resolution** - Automatic handling of ports, protocols, and hostnames
- 🎭 **Icon Integration** - Includes Simple Icons pack (3000+ brand logos)
- 📱 **Responsive Layout** - Works seamlessly on desktop and mobile
- 🐳 **Docker-Ready** - Single-container deployment with nginx
-**Lightweight** - Minimal resources, fast loading
- 🔎 **Search & Filter** - Instant search with keyboard shortcut (press `/`)
- **Connection Details** - Info button shows hostname/port for each service
- 🎮 **Keyboard Navigation** - Arrow keys to navigate, Enter to open, Esc to clear
- 🟢 **Status Indicators** - Optional visual status (online/offline/maintenance)
- 🏥 **Automatic Health Checks** - Real-time ping tests to detect service availability
- 📂 **Service Groups** - Organize services into categorized sections
## Quick Start
### 1. Deploy with Docker Compose
```bash
docker-compose up -d
```
The homepage will be available at `http://localhost:8088`
### 2. Configure Your Services
Edit `services.xml` to add your services organized into groups:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<services>
<group name="Management">
<service
name="Portainer"
proto="https"
port="9443"
logo="portainer.svg"
/>
</group>
<group name="Media">
<service
name="Jellyfin"
proto="http"
port="8096"
host="jellyfin.example.com"
logo="jellyfin.svg"
/>
</group>
</services>
```
Or use services without groups (they'll appear in a single grid):
```xml
<?xml version="1.0" encoding="UTF-8"?>
<services>
<service name="Portainer" proto="https" port="9443" logo="portainer.svg" />
<service name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" />
</services>
```
### 3. Restart to Apply Changes
```bash
docker-compose restart
```
## Configuration Guide
### Service Attributes
| Attribute | Required | Description | Example |
|-----------|----------|-------------|---------|
| `name` | Yes | Display name for the service | `"Nextcloud"` |
| `proto` | No | Protocol (http/https) | `"https"` (default: `"http"`) |
| `port` | No | Port number | `"8080"` |
| `host` | No | Custom hostname or full URL | `"nextcloud.example.com"` |
| `logo` | No | Icon filename in `/logos/` | `"nextcloud.svg"` |
| `status` | No | Set to `"maintenance"` to skip health check | `"maintenance"` |
| `check-health` | No | Enable/disable auto health check | `"true"` (default) or `"false"` |
### URL Resolution Logic
The service URL is built using this priority:
1. **Full URL** - If `host` starts with `http://` or `https://`, use as-is
2. **Hostname with Port** - If `host` contains `:port`, use `proto://host:port`
3. **Hostname Only** - If `host` is set (no port), use `https://host` (ignores `proto` and `port`)
4. **Fallback** - Use current page hostname with specified `proto` and `port`
### Examples
```xml
<!-- Full URL (ignores proto and port) -->
<service name="Example" host="https://example.com/app" />
<!-- Public domain (defaults to HTTPS) -->
<service name="Nextcloud" host="cloud.example.com" logo="nextcloud.svg" />
<!-- Custom port on domain -->
<service name="Jellyfin" host="media.example.com:8096" proto="http" logo="jellyfin.svg" />
<!-- Local service (uses page hostname) -->
<service name="Portainer" proto="https" port="9443" logo="portainer.svg" />
<!-- Service with status indicator -->
<service name="Home Assistant" proto="http" port="8123" logo="homeassistant.svg" status="online" />
```
## Keyboard Shortcuts
- **`/`** - Focus the search bar (press from anywhere)
- **Arrow Keys** - Navigate between service cards (Up/Down/Left/Right)
- **Enter** - Open the selected service in a new tab
- **Esc** - Clear the current selection
## Info Button
When a service has both a hostname and port configured, a small info button (ⓘ) appears in the bottom-right corner of the card. Click it to view connection details including:
- Service name
- Hostname
- Port number
- Protocol
## Status Indicators
Services are automatically checked for availability when the page loads. Status indicators appear in the top-right corner of each card:
```xml
<!-- Automatic health check (default behavior) -->
<service name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" />
<!-- Manual maintenance mode (skips health check) -->
<service name="Nextcloud" status="maintenance" host="cloud.example.com" logo="nextcloud.svg" />
<!-- Disable health check for a service -->
<service name="Legacy App" check-health="false" proto="http" port="8080" logo="app.svg" />
```
Status colors:
- **Gray spinning** (checking) - Currently testing service availability
- **Green pulsing** (online) - Service responded successfully to health check
- **Red** (offline) - Service failed to respond or timed out (5 seconds)
- **Orange** (maintenance) - Manual maintenance mode, health check skipped
### How Health Checks Work
- Each service is automatically pinged when the page loads
- Uses a 5-second timeout per service
- Checks run in parallel for all services
- Services marked `status="maintenance"` skip the health check
- Set `check-health="false"` to disable checking for specific services
- No server-side component needed - runs entirely in the browser
## Service Groups
Organize your services into categorized sections for better organization:
```xml
<services>
<group name="Management">
<service name="Portainer" proto="https" port="9443" logo="portainer.svg" />
<service name="Uptime Kuma" proto="http" port="3001" logo="uptime-kuma.svg" />
</group>
<group name="Media">
<service name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" />
<service name="Transmission" proto="http" port="9091" logo="transmission.svg" />
</group>
<group name="Storage">
<service name="Nextcloud" host="cloud.example.com" logo="nextcloud.svg" />
<service name="FileBrowser" proto="http" port="8986" logo="filebrowser.svg" />
</group>
</services>
```
### Group Features
- **Categorization**: Group related services together (Media, Management, Development, etc.)
- **Visual Separation**: Each group has a styled header with an accent underline
- **Smart Search**: Searching filters services and hides empty groups automatically
- **Backward Compatible**: Services without groups still work (displayed in a single grid)
- **Flexible**: Use as many or as few groups as needed
### Group Tips
- Use clear, descriptive group names (e.g., "Media Services" instead of "Group 1")
- Keep related services together for easier navigation
- Groups appear in the order defined in the XML
- Empty groups (no services) are automatically hidden
## Icon Management
### Using Included Icons
The repository includes Simple Icons (3000+ brand logos). Available icons are in:
```
logos/simple-icons/icons/
```
### Adding Custom Icons
1. Add your SVG file to the `logos/` directory
2. Reference it in `services.xml`:
```xml
<service name="MyApp" logo="myapp.svg" />
```
### Icon Styling
All icons are automatically styled white using CSS filters. To customize:
Edit `styles.css` and modify the `.card .logo` rule:
```css
.card .logo {
filter: brightness(0) invert(1); /* White icons */
/* OR */
filter: hue-rotate(180deg); /* Color shift */
}
```
## Customization
### Theme Colors
Edit CSS variables in `styles.css`:
```css
:root {
--bg: #0f1720; /* Background color */
--card: #0b1220; /* Card background */
--accent: #4f46e5; /* Accent color (purple) */
--muted: #94a3b8; /* Muted text */
}
```
### Holographic Effects
The holographic button effects include:
- **Shimmer Animation** - Continuous light sweep (8s loop, 3s on hover)
- **Gradient Background** - Purple/pink gradient blend
- **Glowing Border** - Animated gradient border on hover
- **Backdrop Blur** - Glass-like frosted effect
To adjust shimmer speed, modify the `@keyframes shimmer` animation:
```css
.card::before {
animation: shimmer 8s infinite linear; /* Change 8s to adjust speed */
}
```
### Layout
Change grid responsiveness in `styles.css`:
```css
.grid {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
/* Adjust minmax() to change card width */
}
```
## File Structure
```
services-homepage/
├── docker-compose.yml # Docker deployment config
├── Dockerfile # Custom nginx build (if needed)
├── index.html # Main HTML page
├── styles.css # Holographic UI styles
├── services.xml # Service definitions
├── README.md # This file
├── LICENSE # License information
└── logos/ # Icon directory
├── simple-icons/ # Simple Icons pack (3000+ logos)
├── default.svg # Fallback icon
├── gitea.svg
├── jellyfin.svg
├── nextcloud.svg
└── ...
```
## Updating Icons
### Replace All Icons with Simple Icons
```bash
cd logos
for f in *.svg; do
basename=$(basename "$f" .svg)
match=$(find simple-icons/icons -type f -iname "*${basename}*.svg" -print -quit)
[ -n "$match" ] && cp "$match" "$f" && echo "Updated: $f"
done
```
### Find Available Icons
```bash
ls logos/simple-icons/icons/ | grep -i "keyword"
```
## Backup and Recovery
Icon backups are automatically created in:
```
logos/backup-YYYYMMDD-HHMMSS/
```
To restore from backup:
```bash
cp logos/backup-20251123-231406/*.svg logos/
docker-compose restart
```
## Troubleshooting
### Cloudflare, HTTPS and Mixed Content
If you're serving the homepage over HTTPS (for example, via Cloudflare), your browser will block active (programmatic) HTTP requests to local IPs — this is "mixed content". That can cause the health checks for local services to fail or to be marked offline.
Recommendations:
- Enable HTTPS for your local services (e.g., configure TLS or use a reverse proxy with a valid certificate) and/or use Cloudflare Tunnel to serve the service with a domain and TLS.
- Or configure a server-side proxy that performs health checks and serves the results over HTTPS (for example, add a proxy endpoint in your nginx config and proxy_pass to the local IP/port); the browser will then make a same-origin secure request to the proxy rather than directly to the IP.
- In `services.xml`, use the `tailscale-ip` attribute to supply an easily-editable Tailscale IP for local services that should be used for links and health checks.
- In `services.xml`, use the `tailscale-ip` attribute to supply an easily-editable Tailscale IP for local services that should be used for links and health checks.
Per-service `local-ip` override:
- If a specific service has a `local-ip` attribute (for example, `local-ip="192.168.2.180"`), the server-side health proxy will use that local IP for the health check. This allows per-service control when the internal IP differs from the household-wide Tailscale IP or when services are bound to different hosts.
Cloudflare Insights & CORS:
- If you see console errors about `static.cloudflareinsights.com` or messages like "CORS request did not succeed" or "integrity mismatch", this is likely a script injected by Cloudflare. This is not part of the homepage codebase and is injected by Cloudflare's edge. You can disable Cloudflare Analytics/Insights or adjust settings in the Cloudflare dashboard to remove or avoid that script if it's causing issues with CSP or integrity.
### Services Not Loading
1. Check `services.xml` syntax:
```bash
xmllint --noout services.xml
```
2. Check browser console for errors (F12)
3. Verify file permissions:
```bash
chmod 644 services.xml index.html styles.css
chmod 755 logos/
```
### Icons Not Displaying
1. Verify icon exists:
```bash
ls -lh logos/youricon.svg
```
2. Check icon reference in `services.xml` matches filename exactly
3. Clear browser cache (Ctrl+Shift+R)
### Container Issues
```bash
# View logs
docker logs services-homepage
# Restart container
docker-compose restart
# Rebuild if files changed
docker-compose up -d --force-recreate
```
## Performance
- **Image Size**: ~45MB (nginx:alpine base)
- **Memory Usage**: ~5-10MB
- **Load Time**: <100ms (local network)
- **Icons**: Cached by browser after first load
## Browser Support
- ✅ Chrome/Edge 90+
- ✅ Firefox 88+
- ✅ Safari 14+
- ✅ Mobile browsers (iOS Safari, Chrome Android)
## Security
- All volumes mounted read-only (`:ro`)
- No external dependencies at runtime
- Logs automatically rotated (5MB max, 2 files)
- CORS not enabled (same-origin only)
## Advanced Usage
### Custom Nginx Config
Create `nginx.conf` and mount it:
```yaml
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
```
### Adding Authentication
Use a reverse proxy (Nginx Proxy Manager, Caddy, Traefik) with basic auth:
```nginx
location / {
auth_basic "Services";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://services-homepage:80;
}
```
### Dynamic Port Updates
For services with dynamic ports (e.g., Transmission behind VPN), use environment variables:
```xml
<service name="Transmission" proto="http" port="${TRANS_PORT}" logo="transmission.svg" />
```
Then update via script or use a template processor.
## Contributing
Contributions welcome! Completed features:
- ✅ Search/filter functionality with keyboard shortcut
- ✅ Keyboard navigation (arrow keys, Enter, Esc)
- ✅ Service status indicators (online/offline/maintenance)
- ✅ Automatic health checks with ping tests
- ✅ Info button showing connection details
- ✅ Service groups/categories
Areas for future improvement:
- Service health checks via custom endpoints
- Drag-and-drop reordering within groups
- Collapsible group sections
- Custom themes/color schemes
- Export/import service configurations
- Dashboard widgets (time, weather, etc.)
## License
MIT License - See LICENSE file for details
## Credits
- **Icons**: [Simple Icons](https://simpleicons.org/) (CC0 1.0 Universal)
- **Design Inspiration**: Holographic UI by vishnu137 on CodePen
- **Web Server**: nginx Alpine
## Support
For issues, questions, or suggestions:
1. Check this README first
2. Review browser console for errors
3. Check Docker logs: `docker logs services-homepage`
4. Verify `services.xml` syntax
---
**Version**: 1.0.0
**Last Updated**: November 23, 2025

78
backend/health-proxy.py Normal file
View File

@@ -0,0 +1,78 @@
from flask import Flask, request, jsonify, abort
import requests
import os
import xml.etree.ElementTree as ET
import time
app = Flask(__name__)
# Optional token for access control (not production secure)
HEALTH_TOKEN = os.environ.get('HEALTH_TOKEN')
@app.route('/healthcheck')
def healthcheck():
token = request.args.get('token') or request.headers.get('X-Health-Token')
if HEALTH_TOKEN and token != HEALTH_TOKEN:
return jsonify({'error': 'Unauthorized'}), 401
# New behavior: prefer service id lookup
service_id = request.args.get('id')
host = request.args.get('host')
port = request.args.get('port')
proto = request.args.get('proto', 'http')
if service_id:
# Load services.xml and find service by id
try:
tree = ET.parse('/usr/share/nginx/html/services.xml')
root = tree.getroot()
except Exception as e:
return jsonify({'error': 'unable to parse services.xml', 'detail': str(e)}), 500
# Find service element by id
svc = None
for s in root.findall('.//service'):
sid = s.get('id') or s.get('name')
if sid and sid == service_id:
svc = s
break
if svc is None:
return jsonify({'error': 'service id not found'}), 404
# Determine host/port/proto preference: local-ip > tailscale-ip root > host attr > fallback to provided host
local_ip = svc.get('local-ip')
root_tailscale = root.get('tailscale-ip')
host_attr = svc.get('host')
proto_attr = svc.get('proto') or proto
port_attr = svc.get('port') or port
health_path = svc.get('health-path', '') # Custom health check path (e.g., /api/health, /ping)
# prefer local-ip if present
target_host = local_ip or root_tailscale or (host_attr.split(':')[0] if host_attr and ':' in host_attr else None) or host
target_port = port_attr or (host_attr.split(':')[1] if host_attr and ':' in host_attr else None)
target_proto = proto_attr
if not target_host or not target_port:
return jsonify({'error': 'service missing host or port mapping'}), 400
host = target_host
port = target_port
proto = target_proto
if not host or not port:
return jsonify({'error': 'missing parameters, expected host and port or id'}), 400
# Build URL with optional custom health path
health_path = health_path if 'health_path' in locals() else ''
url = f"{proto}://{host}:{port}{health_path}"
try:
# Use HEAD to do a lightweight check
r = requests.head(url, timeout=4, allow_redirects=True, verify=False)
# Any 2xx/3xx is considered OK
ok = 200 <= r.status_code < 400
return jsonify({'ok': ok, 'status_code': r.status_code}), (200 if ok else 502)
except requests.RequestException as e:
return jsonify({'ok': False, 'error': str(e)}), 502
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8081)

112
backend/order-service.py Normal file
View File

@@ -0,0 +1,112 @@
from flask import Flask, request, jsonify
import sqlite3
import os
import json
from datetime import datetime
app = Flask(__name__)
# Database setup
DB_PATH = '/data/services-order.db'
os.makedirs('/data', exist_ok=True)
def init_db():
"""Initialize the database with orders table"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS service_order (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL DEFAULT 'default',
order_data TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
# Initialize database on startup
init_db()
@app.route('/api/order', methods=['GET'])
def get_order():
"""Get the saved service order"""
user_id = request.args.get('user_id', 'default')
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
'SELECT order_data FROM service_order WHERE user_id = ? ORDER BY updated_at DESC LIMIT 1',
(user_id,)
)
row = cursor.fetchone()
conn.close()
if row:
return jsonify(json.loads(row[0])), 200
else:
return jsonify({}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/order', methods=['POST'])
def save_order():
"""Save the service order"""
user_id = request.json.get('user_id', 'default')
order_data = request.json.get('order', {})
if not order_data:
return jsonify({'error': 'No order data provided'}), 400
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Check if user already has an order
cursor.execute('SELECT id FROM service_order WHERE user_id = ?', (user_id,))
existing = cursor.fetchone()
if existing:
# Update existing order
cursor.execute(
'UPDATE service_order SET order_data = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?',
(json.dumps(order_data), user_id)
)
else:
# Insert new order
cursor.execute(
'INSERT INTO service_order (user_id, order_data) VALUES (?, ?)',
(user_id, json.dumps(order_data))
)
conn.commit()
conn.close()
return jsonify({'success': True, 'message': 'Order saved'}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/order', methods=['DELETE'])
def delete_order():
"""Delete saved order (reset to default)"""
user_id = request.args.get('user_id', 'default')
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('DELETE FROM service_order WHERE user_id = ?', (user_id,))
conn.commit()
conn.close()
return jsonify({'success': True, 'message': 'Order deleted'}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/health', methods=['GET'])
def health():
"""Health check endpoint"""
return jsonify({'status': 'ok'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8082)

View File

@@ -1,18 +1,50 @@
version: '3.8'
services:
services-homepage:
image: nginx:alpine
build: .
container_name: services-homepage
ports:
- "8088:80"
volumes:
- ./index.html:/usr/share/nginx/html/index.html:ro
- ./services.xml:/usr/share/nginx/html/services.xml:ro
- ./styles.css:/usr/share/nginx/html/styles.css:ro
- ./logos:/usr/share/nginx/html/logos:ro
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "5m"
max-file: "2"
health-proxy:
image: python:3.10-slim
container_name: services-homepage-health-proxy
working_dir: /app
volumes:
- ./backend/health-proxy.py:/app/health-proxy.py:ro
- ./services.xml:/usr/share/nginx/html/services.xml:ro
environment:
- HEALTH_TOKEN=
command: ["sh", "-c", "pip install flask requests && python health-proxy.py"]
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "5m"
max-file: "2"
order-service:
image: python:3.10-slim
container_name: services-homepage-order-service
working_dir: /app
volumes:
- ./backend/order-service.py:/app/order-service.py:ro
- order-data:/data
command: ["sh", "-c", "pip install flask && python order-service.py"]
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "5m"
max-file: "2"
volumes:
order-data:

View File

@@ -4,102 +4,99 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Services Homepage</title>
<!-- PWA Meta Tags -->
<meta name="description" content="Quick access to your self-hosted services" />
<meta name="theme-color" content="#0f3460" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Services" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/logos/icon-192.png" />
<link rel="stylesheet" href="/styles.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body>
<canvas id="galaxy-canvas"></canvas>
<div id="background-overlay"></div>
<header>
<h1>My Services</h1>
<p class="subtitle">Quick links to the commonly used containers on this host</p>
<div class="search-container">
<input type="text" id="search-input" placeholder="🔍 Search services or press Enter to search DuckDuckGo..." />
<button id="ddg-search-btn" title="Search on DuckDuckGo" aria-label="Search on DuckDuckGo">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 23C5.925 23 1 18.075 1 12S5.925 1 12 1s11 4.925 11 11-4.925 11-11 11zm-1.424-8.93c-1.492 0-2.412-1.02-2.412-2.474 0-1.476.93-2.51 2.401-2.51 1.492 0 2.391 1.034 2.391 2.51.011 1.465-.92 2.475-2.38 2.475zm4.703-.004c-1.476 0-2.396-1.016-2.396-2.47 0-1.476.93-2.51 2.401-2.51 1.492 0 2.391 1.034 2.391 2.51 0 1.465-.92 2.47-2.396 2.47zM12 17.906c-3.153 0-4.976-1.951-5.281-3.28h1.097c.322.968 1.645 2.184 4.184 2.184s3.862-1.216 4.184-2.184h1.097c-.305 1.329-2.128 3.28-5.281 3.28z"/>
</svg>
</button>
</div>
</header>
<main>
<section class="grid" id="services-grid">
<!-- Services will be populated dynamically from /services.xml -->
<section id="services-container">
<!-- Service groups will be populated dynamically from /services.xml -->
</section>
<section class="notes">
<h2>Notes</h2>
<ul>
<li>If any service is behind a reverse proxy or uses host networking, the path/host may differ.</li>
<li>Edit <code>services.xml</code> in this repo to add/remove links.</li>
</ul>
<section class="notes" id="readme-section">
<div id="readme-content">Loading documentation...</div>
</section>
</main>
<footer>
<small>Generated: static homepage — served by nginx in a container. Edit and rebuild to update.</small>
</footer>
<!-- JavaScript modules -->
<script src="/js/galaxy-background.js"></script>
<script src="/js/themes.js"></script>
<script src="/js/services-loader.js"></script>
<script src="/js/drag-drop.js"></script>
<script src="/js/collapsible-groups.js"></script>
<script src="/js/search.js"></script>
<script src="/js/keyboard-nav.js"></script>
<script src="/js/readme-loader.js"></script>
<script src="/js/export-import.js"></script>
<script src="/js/widgets.js"></script>
<!-- Initialize features after DOM is loaded -->
<script>
// Fetch services.xml and render the service cards with logos.
(async function(){
const grid = document.getElementById('services-grid');
const host = window.location.hostname;
try{
const res = await fetch('/services.xml', {cache: 'no-cache'});
if(!res.ok){ throw new Error('Failed to load services.xml'); }
const text = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'application/xml');
const services = Array.from(doc.getElementsByTagName('service'));
if(services.length===0){ grid.innerHTML = '<p class="notes">No services found in services.xml</p>'; return; }
services.forEach(s=>{
const name = s.getAttribute('name') || s.getAttribute('id') || 'unknown';
const proto = s.getAttribute('proto') || 'http';
const port = s.getAttribute('port') || '';
const logo = s.getAttribute('logo') || '';
const hostAttr = s.getAttribute('host'); // optional public hostname or full URL
// Build href: prefer explicit host attribute when present.
// Rules:
// - If hostAttr starts with http(s)://, use it as-is.
// - If hostAttr is a hostname (no protocol) and does NOT include a port, prefer HTTPS and DO NOT append the service port.
// - If hostAttr includes a port (example.com:8096), use it as provided (no extra port appended).
// - If no hostAttr, fall back to current page hostname and use the service port.
let href = '';
if(hostAttr){
if(/^https?:\/\//i.test(hostAttr)){
href = hostAttr;
} else {
const hasPortInHost = /:\d+$/.test(hostAttr);
if(hasPortInHost){
// host includes port, use the host as provided with the proto
href = `${proto}://${hostAttr}`;
} else {
// Hostname without port: prefer HTTPS and do NOT append numeric port.
href = `https://${hostAttr}`;
}
}
} else {
// Fallback to current page hostname and include port if non-standard
let portPart = '';
if(port && !((proto==='http'&&port==='80')||(proto==='https'&&port==='443'))){ portPart = ':'+port; }
href = `${proto}://${host}${portPart}`;
// Register Service Worker for PWA
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('Service Worker registered:', registration.scope);
})
.catch((error) => {
console.log('Service Worker registration failed:', error);
});
});
}
window.addEventListener('DOMContentLoaded', () => {
// Wait for services to be loaded
const checkServicesLoaded = setInterval(() => {
if (window.servicesData && window.servicesData.allServices.length > 0) {
clearInterval(checkServicesLoaded);
// Initialize all features
if (window.themesModule) window.themesModule.init();
if (window.dragDropModule) {
window.dragDropModule.init();
window.dragDropModule.applySavedOrder();
}
const a = document.createElement('a');
a.className = 'card';
a.href = href;
a.target = '_blank';
a.rel = 'noreferrer';
const img = document.createElement('img');
img.className = 'logo';
img.src = logo ? ('/logos/'+logo) : '/logos/default.svg';
img.alt = name + ' logo';
const span = document.createElement('div');
span.className = 'label';
span.textContent = `${name}`;
a.appendChild(img);
a.appendChild(span);
grid.appendChild(a);
});
}catch(err){
console.error(err);
grid.innerHTML = '<p class="notes">Error loading services.xml — see console for details.</p>';
}
})();
if (window.collapsibleGroupsModule) window.collapsibleGroupsModule.init();
if (window.importExportModule) window.importExportModule.init();
if (window.widgetsModule) window.widgetsModule.init();
}
}, 100);
// Timeout after 5 seconds
setTimeout(() => clearInterval(checkServicesLoaded), 5000);
});
</script>
</body>
</html>

93
js/collapsible-groups.js Normal file
View File

@@ -0,0 +1,93 @@
/**
* collapsible-groups.js - Collapsible service groups
*/
const STORAGE_KEY_COLLAPSED = 'collapsed-groups';
// Load collapsed state from localStorage
function loadCollapsedState() {
try {
const saved = localStorage.getItem(STORAGE_KEY_COLLAPSED);
return saved ? JSON.parse(saved) : {};
} catch (e) {
console.error('Error loading collapsed state:', e);
return {};
}
}
// Save collapsed state to localStorage
function saveCollapsedState(collapsedMap) {
try {
localStorage.setItem(STORAGE_KEY_COLLAPSED, JSON.stringify(collapsedMap));
} catch (e) {
console.error('Error saving collapsed state:', e);
}
}
// Toggle group collapse
function toggleGroup(groupElement) {
const groupName = groupElement.querySelector('h2')?.textContent;
if (!groupName) return;
const content = groupElement.querySelector('.services-grid');
const toggle = groupElement.querySelector('.group-toggle');
if (!content) return;
const isCollapsed = content.classList.toggle('collapsed');
toggle?.classList.toggle('collapsed', isCollapsed);
// Save state
const collapsedMap = loadCollapsedState();
collapsedMap[groupName] = isCollapsed;
saveCollapsedState(collapsedMap);
}
// Apply saved collapsed state
function applyCollapsedState() {
const collapsedMap = loadCollapsedState();
document.querySelectorAll('.service-group').forEach(group => {
const groupName = group.querySelector('h2')?.textContent;
if (!groupName) return;
const content = group.querySelector('.services-grid');
const toggle = group.querySelector('.group-toggle');
if (collapsedMap[groupName]) {
content?.classList.add('collapsed');
toggle?.classList.add('collapsed');
}
});
}
// Initialize collapsible groups
function initCollapsibleGroups() {
document.querySelectorAll('.service-group').forEach(group => {
const header = group.querySelector('h2');
if (!header) return;
// Add toggle icon
const toggle = document.createElement('span');
toggle.className = 'group-toggle';
toggle.innerHTML = '▼';
toggle.title = 'Collapse/Expand';
// Make header clickable
header.style.cursor = 'pointer';
header.style.userSelect = 'none';
header.insertBefore(toggle, header.firstChild);
// Add click handler
header.addEventListener('click', () => toggleGroup(group));
});
// Apply saved state
applyCollapsedState();
}
// Export for use in other modules
window.collapsibleGroupsModule = {
init: initCollapsibleGroups,
applyCollapsedState: applyCollapsedState
};

205
js/drag-drop.js Normal file
View File

@@ -0,0 +1,205 @@
/**
* drag-drop.js - Drag and drop service reordering
*/
const STORAGE_KEY_ORDER = 'services-order';
const USER_ID = 'default'; // Could be customized per user in the future
// Load saved order from server (with localStorage fallback)
async function loadSavedOrder() {
try {
// Try to load from server first
const response = await fetch(`/api/order?user_id=${USER_ID}`);
if (response.ok) {
const serverOrder = await response.json();
if (Object.keys(serverOrder).length > 0) {
// Sync server order to localStorage
localStorage.setItem(STORAGE_KEY_ORDER, JSON.stringify(serverOrder));
return serverOrder;
}
}
// Fallback to localStorage
const saved = localStorage.getItem(STORAGE_KEY_ORDER);
return saved ? JSON.parse(saved) : {};
} catch (e) {
console.error('Error loading saved order:', e);
// Fallback to localStorage
try {
const saved = localStorage.getItem(STORAGE_KEY_ORDER);
return saved ? JSON.parse(saved) : {};
} catch (e2) {
return {};
}
}
}
// Save order to server and localStorage
async function saveSavedOrder(orderMap) {
try {
// Save to localStorage immediately
localStorage.setItem(STORAGE_KEY_ORDER, JSON.stringify(orderMap));
// Also save to server
const response = await fetch('/api/order', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: USER_ID,
order: orderMap
})
});
if (!response.ok) {
console.warn('Failed to save order to server, using localStorage only');
}
} catch (e) {
console.error('Error saving order to server:', e);
// localStorage save already happened above, so at least we have local persistence
}
}
// Apply saved order to service groups
async function applySavedOrder() {
const orderMap = await loadSavedOrder();
document.querySelectorAll('.service-group').forEach(group => {
const groupName = group.querySelector('h2')?.textContent;
if (!groupName || !orderMap[groupName]) return;
const container = group.querySelector('.services-grid');
if (!container) return;
const cards = Array.from(container.querySelectorAll('.service-card'));
const orderedIds = orderMap[groupName];
// Sort cards based on saved order
cards.sort((a, b) => {
const aId = a.dataset.serviceId;
const bId = b.dataset.serviceId;
const aIndex = orderedIds.indexOf(aId);
const bIndex = orderedIds.indexOf(bId);
if (aIndex === -1 && bIndex === -1) return 0;
if (aIndex === -1) return 1;
if (bIndex === -1) return -1;
return aIndex - bIndex;
});
// Re-append in sorted order
cards.forEach(card => container.appendChild(card));
});
}
// Initialize drag and drop
function initDragDrop() {
let draggedElement = null;
let draggedGroup = null;
document.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('service-card')) {
draggedElement = e.target;
draggedGroup = e.target.closest('.service-group');
e.target.style.opacity = '0.5';
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.innerHTML);
}
});
document.addEventListener('dragend', (e) => {
if (e.target.classList.contains('service-card')) {
e.target.style.opacity = '1';
// Remove all drop indicators
document.querySelectorAll('.service-card').forEach(card => {
card.classList.remove('drag-over');
});
// Save the new order
saveCurrentOrder();
}
});
document.addEventListener('dragover', (e) => {
if (e.preventDefault) {
e.preventDefault();
}
const target = e.target.closest('.service-card');
if (target && target !== draggedElement && draggedElement) {
const targetGroup = target.closest('.service-group');
// Only allow reordering within the same group
if (targetGroup === draggedGroup) {
e.dataTransfer.dropEffect = 'move';
// Remove previous indicators
document.querySelectorAll('.service-card').forEach(card => {
card.classList.remove('drag-over');
});
// Add indicator
target.classList.add('drag-over');
}
}
return false;
});
document.addEventListener('drop', (e) => {
if (e.stopPropagation) {
e.stopPropagation();
}
const target = e.target.closest('.service-card');
if (target && target !== draggedElement && draggedElement) {
const targetGroup = target.closest('.service-group');
// Only allow dropping within the same group
if (targetGroup === draggedGroup) {
const container = target.parentNode;
const targetIndex = Array.from(container.children).indexOf(target);
const draggedIndex = Array.from(container.children).indexOf(draggedElement);
if (draggedIndex < targetIndex) {
container.insertBefore(draggedElement, target.nextSibling);
} else {
container.insertBefore(draggedElement, target);
}
}
}
target?.classList.remove('drag-over');
return false;
});
// Make service cards draggable
document.querySelectorAll('.service-card').forEach(card => {
card.setAttribute('draggable', 'true');
});
}
// Save current order of all groups
function saveCurrentOrder() {
const orderMap = {};
document.querySelectorAll('.service-group').forEach(group => {
const groupName = group.querySelector('h2')?.textContent;
if (!groupName) return;
const cards = group.querySelectorAll('.service-card');
const order = Array.from(cards).map(card => card.dataset.serviceId);
orderMap[groupName] = order;
});
saveSavedOrder(orderMap);
}
// Export for use in other modules
window.dragDropModule = {
init: initDragDrop,
applySavedOrder: applySavedOrder
};

195
js/export-import.js Normal file
View File

@@ -0,0 +1,195 @@
/**
* export-import.js - Export and import service configurations
*/
// Export services configuration to JSON
function exportConfiguration() {
// Fetch services.xml and convert to JSON
fetch('/services.xml')
.then(response => response.text())
.then(xmlText => {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
const config = {
version: '1.0',
exportDate: new Date().toISOString(),
tailscaleIp: xmlDoc.documentElement.getAttribute('tailscale-ip'),
groups: []
};
// Parse groups
const groups = xmlDoc.querySelectorAll('group');
groups.forEach(group => {
const groupData = {
name: group.getAttribute('name'),
services: []
};
const services = group.querySelectorAll('service');
services.forEach(service => {
const serviceData = {};
// Get all attributes
Array.from(service.attributes).forEach(attr => {
serviceData[attr.name] = attr.value;
});
groupData.services.push(serviceData);
});
config.groups.push(groupData);
});
// Download JSON file
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `services-config-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification('Configuration exported successfully!', 'success');
})
.catch(error => {
console.error('Export error:', error);
showNotification('Failed to export configuration', 'error');
});
}
// Import services configuration from JSON
function importConfiguration(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const config = JSON.parse(e.target.result);
// Validate config structure
if (!config.version || !config.groups) {
throw new Error('Invalid configuration format');
}
// Convert JSON back to XML
let xml = '<?xml version="1.0" encoding="utf-8"?>\n';
xml += `<services${config.tailscaleIp ? ` tailscale-ip="${config.tailscaleIp}"` : ''}>\n`;
config.groups.forEach(group => {
xml += ` <group name="${escapeXml(group.name)}">\n`;
group.services.forEach(service => {
xml += ' <service';
Object.entries(service).forEach(([key, value]) => {
xml += ` ${key}="${escapeXml(value)}"`;
});
xml += ' />\n';
});
xml += ' </group>\n';
});
xml += '</services>\n';
// Download the generated XML
const blob = new Blob([xml], { type: 'text/xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'services.xml';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification('Configuration imported! Download services.xml and replace the existing file.', 'success');
} catch (error) {
console.error('Import error:', error);
showNotification('Failed to import configuration: ' + error.message, 'error');
}
};
reader.readAsText(file);
}
// Escape XML special characters
function escapeXml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&apos;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
// Show notification
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
// Trigger animation
setTimeout(() => notification.classList.add('show'), 10);
// Remove after 3 seconds
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Create import/export UI
function createImportExportUI() {
const container = document.createElement('div');
container.className = 'import-export-controls';
const exportBtn = document.createElement('button');
exportBtn.className = 'control-btn';
exportBtn.innerHTML = '📥 Export';
exportBtn.title = 'Export configuration to JSON';
exportBtn.addEventListener('click', exportConfiguration);
const importBtn = document.createElement('button');
importBtn.className = 'control-btn';
importBtn.innerHTML = '📤 Import';
importBtn.title = 'Import configuration from JSON';
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.style.display = 'none';
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
importConfiguration(e.target.files[0]);
}
});
importBtn.addEventListener('click', () => fileInput.click());
container.appendChild(exportBtn);
container.appendChild(importBtn);
container.appendChild(fileInput);
return container;
}
// Initialize import/export functionality
function initImportExport() {
const header = document.querySelector('header');
if (header) {
const ui = createImportExportUI();
header.appendChild(ui);
}
}
// Export for use in other modules
window.importExportModule = {
init: initImportExport,
exportConfiguration: exportConfiguration
};

306
js/galaxy-background.js Normal file
View File

@@ -0,0 +1,306 @@
// Galaxy background - moonlit terrain with floating stars
// Includes FPS monitoring and fallback to simple background
(function initGalaxy(){
console.clear();
// FPS monitoring
let lastTime = performance.now();
let frameCount = 0;
let fps = 60;
let lowFpsCount = 0;
const FPS_THRESHOLD = 27;
const LOW_FPS_SAMPLES = 5; // Switch after 5 consecutive low FPS readings
let isSimpleMode = false;
var ww = window.innerWidth,
wh = window.innerHeight;
var renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: document.querySelector('#galaxy-canvas')
});
renderer.setSize(ww, wh);
renderer.setClearColor(0x001a2d);
var scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x001a2d, 80, 140);
var camera = new THREE.PerspectiveCamera(45, ww/wh, 0.1, 200);
camera.position.x = 70;
camera.position.y = 30;
camera.position.z = 5;
camera.lookAt(new THREE.Vector3());
/* LIGHTS */
var moonLight = new THREE.PointLight(0xffffff, 2, 150);
scene.add(moonLight);
var moon;
function createMoon() {
var geometry = new THREE.SphereGeometry(8, 32, 32);
var material = new THREE.MeshPhongMaterial({
color: 0x26fdd9,
shininess: 15,
emissive: 0x2bb2e6,
emissiveIntensity: 0.8
});
moon = new THREE.Mesh(geometry, material);
moon.position.x = -9;
moon.position.z = -6.5;
moon.position.y = 1;
moon.rotation.y = -1;
scene.add(moon);
moonLight.position.copy(moon.position);
moonLight.position.y += 4;
var moonLight2 = new THREE.PointLight(0xffffff, 0.6, 150);
scene.add(moonLight2);
moonLight2.position.x += 20;
moonLight2.position.y -= 20;
moonLight2.position.z -= 25;
}
// Initialize simplex noise
var noise = new SimplexNoise();
function createTerrain() {
var geometry = new THREE.PlaneGeometry(150, 150, 120, 120);
var m = new THREE.Matrix4();
m.makeRotationX(Math.PI * -0.5);
geometry.applyMatrix4(m);
var positions = geometry.attributes.position;
for(var i = 0; i < positions.count; i++) {
var x = positions.getX(i);
var z = positions.getZ(i);
var ratio = noise.noise3D(x * 0.03, z * 0.03, 0);
positions.setY(i, ratio * 10);
}
positions.needsUpdate = true;
geometry.computeVertexNormals();
var material = new THREE.MeshPhongMaterial({
color: 0x198257,
emissive: 0x032f50
});
var plane = new THREE.Mesh(geometry, material);
scene.add(plane);
}
var stars = new THREE.Group();
scene.add(stars);
var starsLights = new THREE.Group();
scene.add(starsLights);
var starsAmount = 20;
function createStars() {
var geometry = new THREE.SphereGeometry(0.3, 16, 16);
var material = new THREE.MeshBasicMaterial({color: 0xffffff});
for(var i = 0; i < starsAmount; i++) {
var star = new THREE.Mesh(geometry, material);
star.position.x = (Math.random() - 0.5) * 150;
star.position.z = (Math.random() - 0.5) * 150;
var ratio = noise.noise3D(star.position.x * 0.03, star.position.z * 0.03, 0);
star.position.y = ratio * 10 + 0.3;
stars.add(star);
var velX = (Math.random() + 0.1) * 0.1 * (Math.random() < 0.5 ? -1 : 1);
var velY = (Math.random() + 0.1) * 0.1 * (Math.random() < 0.5 ? -1 : 1);
star.vel = new THREE.Vector2(velX, velY);
var starLight = new THREE.PointLight(0xffffff, 0.8, 3);
starLight.position.copy(star.position);
starLight.position.y += 0.5;
starsLights.add(starLight);
}
}
function updateStar(star, index) {
if(star.position.x < -75) {
star.position.x = 75;
}
if(star.position.x > 75) {
star.position.x = -75;
}
if(star.position.z < -75) {
star.position.z = 75;
}
if(star.position.z > 75) {
star.position.z = -75;
}
star.position.x += star.vel.x;
star.position.z += star.vel.y;
var ratio = noise.noise3D(star.position.x * 0.03, star.position.z * 0.03, 0);
star.position.y = ratio * 10 + 0.3;
starsLights.children[index].position.copy(star.position);
starsLights.children[index].position.y += 0.5;
}
function render(a) {
requestAnimationFrame(render);
if (!isSimpleMode) {
// FPS monitoring (only when not in simple mode)
frameCount++;
const currentTime = performance.now();
const delta = currentTime - lastTime;
if (delta >= 500) { // Update FPS every 500ms
fps = Math.round((frameCount * 1000) / delta);
console.log(`FPS: ${fps} (threshold: ${FPS_THRESHOLD}, low samples: ${lowFpsCount}/${LOW_FPS_SAMPLES})`);
frameCount = 0;
lastTime = currentTime;
// Check if FPS is consistently low
if (fps < FPS_THRESHOLD) {
lowFpsCount++;
console.warn(`⚠️ Low FPS detected! ${fps} FPS (${lowFpsCount}/${LOW_FPS_SAMPLES})`);
if (lowFpsCount >= LOW_FPS_SAMPLES) {
console.warn(`🔄 Switching to simple background mode...`);
switchToSimpleBackground();
isSimpleMode = true;
// Stop monitoring - mode persists until page refresh
}
} else {
lowFpsCount = 0; // Reset counter if FPS improves
}
}
// Render 3D scene
for(var i = 0; i < starsAmount; i++) {
updateStar(stars.children[i], i);
}
renderer.render(scene, camera);
}
// If in simple mode, just keep the animation frame loop running but do nothing
}
function switchToSimpleBackground() {
console.log('🎨 Activating simple background mode...');
// Stop rendering 3D scene
const canvas = document.querySelector('#galaxy-canvas');
canvas.style.display = 'none';
console.log('✓ 3D canvas hidden');
// Create simple gradient background
const overlay = document.querySelector('#background-overlay');
overlay.style.background = 'linear-gradient(180deg, #0a1628 0%, #001a2d 50%, #000d1a 100%)';
overlay.style.backdropFilter = 'none';
console.log('✓ Gradient background applied');
// Disable all animations and transitions
disableAnimations();
console.log('✓ Animations disabled');
// Add simple animated stars
createSimpleStars();
console.log('✓ Simple stars created');
console.log('✅ Simple background mode activated - animations disabled for performance');
}
function disableAnimations() {
// Create a style tag to disable animations
const style = document.createElement('style');
style.id = 'low-fps-mode';
style.textContent = `
/* Disable all animations and transitions in low FPS mode */
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
/* Remove hover effects and transforms */
.service-card:hover,
.card:hover {
transform: none !important;
box-shadow: 0 4px 12px rgba(79,70,229,0.3) !important;
}
.control-btn:hover,
.theme-toggle:hover,
#ddg-search-btn:hover {
transform: none !important;
}
/* Disable status dot animations */
.status-dot {
animation: none !important;
}
/* Disable shimmer effects */
.service-card::before,
.card::before {
display: none !important;
}
/* Simplify search input focus */
#search-input:focus {
box-shadow: 0 0 0 2px rgba(79,70,229,0.3) !important;
}
`;
document.head.appendChild(style);
}
function createSimpleStars() {
const starsContainer = document.createElement('div');
starsContainer.id = 'simple-stars';
starsContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
`;
// Create 50 simple star divs
for (let i = 0; i < 50; i++) {
const star = document.createElement('div');
star.style.cssText = `
position: absolute;
width: 2px;
height: 2px;
background: white;
border-radius: 50%;
top: ${Math.random() * 100}%;
left: ${Math.random() * 100}%;
opacity: ${Math.random() * 0.5 + 0.3};
animation: twinkle ${Math.random() * 3 + 2}s infinite ease-in-out;
`;
starsContainer.appendChild(star);
}
// Add twinkle animation
if (!document.querySelector('#simple-stars-style')) {
const style = document.createElement('style');
style.id = 'simple-stars-style';
style.textContent = `
@keyframes twinkle {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(starsContainer);
}
function onResize() {
ww = window.innerWidth;
wh = window.innerHeight;
camera.aspect = ww / wh;
camera.updateProjectionMatrix();
renderer.setSize(ww, wh);
}
createMoon();
createTerrain();
createStars();
requestAnimationFrame(render);
window.addEventListener('resize', onResize);
})();

54
js/keyboard-nav.js Normal file
View File

@@ -0,0 +1,54 @@
// Keyboard navigation for service cards
(function initKeyboardNav(){
let selectedIndex = -1;
const searchInput = document.getElementById('search-input');
// Wait for services to load
const checkServicesLoaded = setInterval(() => {
if(window.servicesData){
clearInterval(checkServicesLoaded);
setupKeyboardNav();
}
}, 100);
function setupKeyboardNav(){
const { allServices } = window.servicesData;
const selectCard = (index) => {
const visibleCards = allServices.filter(card => card.style.display !== 'none');
if(visibleCards.length === 0) return;
// Remove previous selection
visibleCards.forEach(card => card.classList.remove('selected'));
// Update index
if(index < 0) index = visibleCards.length - 1;
if(index >= visibleCards.length) index = 0;
selectedIndex = index;
// Add selection
visibleCards[selectedIndex].classList.add('selected');
visibleCards[selectedIndex].scrollIntoView({behavior: 'smooth', block: 'nearest'});
};
document.addEventListener('keydown', (e) => {
if(document.activeElement === searchInput) return;
const visibleCards = allServices.filter(card => card.style.display !== 'none');
if(e.key === 'ArrowRight' || e.key === 'ArrowDown'){
e.preventDefault();
selectCard(selectedIndex + 1);
} else if(e.key === 'ArrowLeft' || e.key === 'ArrowUp'){
e.preventDefault();
selectCard(selectedIndex - 1);
} else if(e.key === 'Enter' && selectedIndex >= 0){
e.preventDefault();
visibleCards[selectedIndex].click();
} else if(e.key === 'Escape'){
visibleCards.forEach(card => card.classList.remove('selected'));
selectedIndex = -1;
}
});
}
})();

96
js/readme-loader.js Normal file
View File

@@ -0,0 +1,96 @@
// Markdown navigation system
(function initMarkdownNav() {
let currentFile = 'FAQ.md';
const defaultFile = 'FAQ.md';
// Available markdown files for navigation
const availableFiles = [
'FAQ.md',
'README.md',
'FEATURES.md',
'QUICK-REFERENCE.md',
'ORDER-PERSISTENCE.md',
'IMPLEMENTATION-SUMMARY.md'
];
async function loadMarkdown(filename) {
const container = document.getElementById('readme-content');
try {
const response = await fetch(`/${filename}`, {cache: 'no-cache'});
if (!response.ok) throw new Error(`${filename} not found`);
const markdown = await response.text();
let html = marked.parse(markdown);
// Convert .md links to clickable navigation
html = html.replace(/href="([^"]+\.md)"/g, (match, mdFile) => {
const basename = mdFile.split('/').pop();
if (availableFiles.includes(basename)) {
return `href="#" data-md-file="${basename}"`;
}
return match;
});
container.innerHTML = html;
currentFile = filename;
// Update home button visibility
updateHomeButton();
// Add click handlers to markdown links
attachMarkdownLinks();
} catch (err) {
console.error('Error loading markdown:', err);
container.innerHTML = `<p>❌ Documentation unavailable: ${filename}</p>`;
}
}
function updateHomeButton() {
let homeBtn = document.getElementById('md-home-btn');
if (currentFile === defaultFile) {
// Hide home button on default page
if (homeBtn) homeBtn.style.display = 'none';
} else {
// Show/create home button on other pages
if (!homeBtn) {
homeBtn = document.createElement('button');
homeBtn.id = 'md-home-btn';
homeBtn.className = 'md-nav-btn';
homeBtn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
<span>Back to FAQ</span>
`;
homeBtn.title = 'Back to FAQ';
homeBtn.addEventListener('click', () => loadMarkdown(defaultFile));
const readmeSection = document.getElementById('readme-section');
readmeSection.insertBefore(homeBtn, readmeSection.firstChild);
}
homeBtn.style.display = 'flex';
}
}
function attachMarkdownLinks() {
const links = document.querySelectorAll('#readme-content a[data-md-file]');
links.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const mdFile = link.getAttribute('data-md-file');
if (mdFile && availableFiles.includes(mdFile)) {
loadMarkdown(mdFile);
// Scroll to readme section
document.getElementById('readme-section').scrollIntoView({ behavior: 'smooth' });
}
});
});
}
// Initial load
loadMarkdown(defaultFile);
})();

84
js/search.js Normal file
View File

@@ -0,0 +1,84 @@
// Search functionality
(function initSearch(){
const searchInput = document.getElementById('search-input');
const ddgButton = document.getElementById('ddg-search-btn');
// DuckDuckGo search function
function searchDuckDuckGo() {
const query = searchInput.value.trim();
if (query) {
const ddgUrl = `https://duckduckgo.com/?q=${encodeURIComponent(query)}`;
window.open(ddgUrl, '_blank', 'noopener,noreferrer');
}
}
// Add click handler for DDG button
if (ddgButton) {
ddgButton.addEventListener('click', (e) => {
e.preventDefault();
searchDuckDuckGo();
});
}
// Add Enter key handler for DDG search
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
searchDuckDuckGo();
}
});
// Wait for services to load
const checkServicesLoaded = setInterval(() => {
if(window.servicesData){
clearInterval(checkServicesLoaded);
setupSearch();
}
}, 100);
function setupSearch(){
const { allServices, container } = window.servicesData;
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase().trim();
let visibleCount = 0;
// Also hide/show group headers
const groupSections = container.querySelectorAll('.service-group');
allServices.forEach(card => {
const serviceName = card.dataset.serviceName;
if(serviceName.includes(searchTerm)){
card.style.display = '';
visibleCount++;
} else {
card.style.display = 'none';
}
});
// Hide empty groups
groupSections.forEach(section => {
const visibleCards = section.querySelectorAll('.service-card:not([style*="display: none"])');
section.style.display = visibleCards.length > 0 ? '' : 'none';
});
// Show message if no results
const existingMsg = container.querySelector('.no-results');
if(existingMsg) existingMsg.remove();
if(visibleCount === 0 && searchTerm !== ''){
const msg = document.createElement('p');
msg.className = 'notes no-results';
msg.textContent = `No services found matching "${e.target.value}". Press Enter to search on DuckDuckGo.`;
container.appendChild(msg);
}
});
// Focus search on '/' key
document.addEventListener('keydown', (e) => {
if(e.key === '/' && document.activeElement !== searchInput){
e.preventDefault();
searchInput.focus();
}
});
}
})();

267
js/services-loader.js Normal file
View File

@@ -0,0 +1,267 @@
// Fetch services.xml and render the service cards with logos.
(async function(){
const container = document.getElementById('services-container');
const searchInput = document.getElementById('search-input');
const host = window.location.hostname;
let allServices = []; // Store all service elements for filtering
try{
const res = await fetch('/services.xml', {cache: 'no-cache'});
if(!res.ok){ throw new Error('Failed to load services.xml'); }
const text = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'application/xml');
// Check for XML parsing errors
const parseError = doc.querySelector('parsererror');
if(parseError){
throw new Error('XML parsing error: ' + parseError.textContent);
}
// Get Tailscale IP from root services element
const servicesRoot = doc.getElementsByTagName('services')[0];
const tailscaleIP = servicesRoot ? servicesRoot.getAttribute('tailscale-ip') : null;
console.log('Tailscale IP:', tailscaleIP || 'not configured');
// Check if we have groups or just services
const groups = Array.from(doc.getElementsByTagName('group'));
const hasGroups = groups.length > 0;
console.log('Found', groups.length, 'groups');
if(hasGroups){
// Render grouped services
groups.forEach(group => {
const groupName = group.getAttribute('name') || 'Services';
const services = Array.from(group.getElementsByTagName('service'));
console.log('Group:', groupName, 'has', services.length, 'services');
if(services.length === 0) return;
// Create group section
const groupSection = document.createElement('section');
groupSection.className = 'service-group';
const groupHeader = document.createElement('h2');
groupHeader.className = 'group-header';
groupHeader.textContent = groupName;
groupSection.appendChild(groupHeader);
const grid = document.createElement('div');
grid.className = 'services-grid';
services.forEach(s => {
const card = createServiceCard(s, host, allServices, tailscaleIP);
grid.appendChild(card);
});
groupSection.appendChild(grid);
container.appendChild(groupSection);
});
} else {
// Fallback: render ungrouped services
const services = Array.from(doc.getElementsByTagName('service'));
if(services.length === 0){
container.innerHTML = '<p class="notes">No services found in services.xml</p>';
return;
}
const grid = document.createElement('div');
grid.className = 'grid';
grid.id = 'services-grid';
services.forEach(s => {
const card = createServiceCard(s, host, allServices, tailscaleIP);
grid.appendChild(card);
});
container.appendChild(grid);
}
// Function to create a service card
function createServiceCard(s, host, allServices, tailscaleIP) {
const name = s.getAttribute('name') || s.getAttribute('id') || 'unknown';
const serviceId = s.getAttribute('id') || name.toLowerCase().replace(/\s+/g, '-');
const proto = s.getAttribute('proto') || 'http';
const port = s.getAttribute('port') || '';
const logo = s.getAttribute('logo') || '';
const hostAttr = s.getAttribute('host'); // optional public hostname or full URL
const localIpAttr = s.getAttribute('local-ip'); // optional local IP for proxied health-check
const manualStatus = s.getAttribute('status'); // optional: 'online', 'offline', 'maintenance'
const checkHealth = s.getAttribute('check-health') !== 'false'; // default true, set to false to disable
// Build href: prefer explicit host attribute when present.
let href = '';
let healthCheckUrl = ''; // Separate URL for health checks
const pageIsSecure = window.location.protocol === 'https:';
const desiredProto = pageIsSecure ? 'https' : proto; // use https for health checks when page is secure to avoid mixed content
if(hostAttr){
// Service has a public hostname or URL
// Parse hostAttr to extract hostname and optional port
let parsedHost = hostAttr;
let parsedPort = port;
let parsedProto = proto;
if(/^https?:\/\//i.test(hostAttr)){
// remove protocol
const u = new URL(hostAttr);
parsedHost = u.hostname;
parsedPort = u.port || parsedPort;
parsedProto = u.protocol.replace(':','') || parsedProto;
href = hostAttr;
} else {
const hasPortInHost = /:\d+$/.test(hostAttr);
if(hasPortInHost){
// host:port
href = `${proto}://${hostAttr}`;
parsedHost = hostAttr.split(':')[0];
parsedPort = hostAttr.split(':')[1];
} else {
href = `https://${hostAttr}`;
parsedHost = hostAttr;
}
}
// Always proxy health checks via same-origin endpoint to avoid mixed-content
healthCheckUrl = `/healthcheck?id=${encodeURIComponent(serviceId)}`;
} else {
// Local service - use Tailscale IP if configured, otherwise current hostname
const targetHost = localIpAttr || tailscaleIP || host;
let portPart = '';
if(port && !((proto==='http'&&port==='80')||(proto==='https'&&port==='443'))){ portPart = ':'+port; }
// Keep the link protocol as the defined proto (so clicking uses the intended protocol),
// but use the server-side health proxy for health checks to avoid mixed content blocking.
href = `${proto}://${targetHost}${portPart}`;
// Use our same-origin proxy path which nginx will forward to the health-proxy service.
const encodedHost = encodeURIComponent(targetHost);
const hcPort = port || (desiredProto === 'https' ? '443' : '80');
if(localIpAttr){
console.log(`Service ${name}: using local-ip ${localIpAttr} for proxied health checks`);
} else if(tailscaleIP){
console.log(`Service ${name}: using tailscale-ip ${tailscaleIP} for proxied health checks`);
}
healthCheckUrl = `/healthcheck?id=${encodeURIComponent(serviceId)}`;
// Warn when site is secure but service link is HTTP and target is a private IP
if(pageIsSecure && proto === 'http' && targetHost === tailscaleIP){
console.warn(`Service ${name} will use HTTP at ${href}, but the page is loaded over HTTPS — mixed-content checks will prevent programmatic health checks to the service. Consider enabling TLS or providing a public hostname.`);
}
}
const a = document.createElement('a');
a.className = 'service-card';
a.href = href;
a.target = '_blank';
a.rel = 'noreferrer';
a.dataset.serviceName = name.toLowerCase(); // For search filtering
a.dataset.serviceId = serviceId; // For drag-drop and identification
a.setAttribute('draggable', 'true'); // Enable drag-and-drop
const img = document.createElement('img');
img.className = 'logo';
img.src = logo ? ('/logos/'+logo) : '/logos/default.svg';
img.alt = name + ' logo';
const span = document.createElement('div');
span.className = 'label';
span.textContent = `${name}`;
a.appendChild(img);
a.appendChild(span);
// Add status indicator - will be updated by health check
const statusDot = document.createElement('span');
statusDot.className = 'status-dot status-checking';
statusDot.title = 'Checking status...';
let currentStatus = 'checking';
// If maintenance mode is set manually, use that and skip health check
if(manualStatus === 'maintenance'){
statusDot.className = 'status-dot status-maintenance';
statusDot.title = 'Status: maintenance';
currentStatus = 'maintenance';
a.appendChild(statusDot);
} else if(checkHealth && healthCheckUrl){
// Show checking indicator initially
a.appendChild(statusDot);
// Perform health check using healthCheckUrl (Tailscale IP for local services)
if(healthCheckUrl !== href){
console.log(`Service ${name}: using healthCheckUrl (${healthCheckUrl}) while link is ${href}`);
}
(async function performHealthCheck(){
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
const response = await fetch(healthCheckUrl, {
method: 'HEAD',
mode: 'no-cors', // Allow cross-origin requests
cache: 'no-cache',
signal: controller.signal
});
clearTimeout(timeoutId);
// In no-cors mode, opaque responses mean the server responded
currentStatus = 'online';
statusDot.className = 'status-dot status-online';
statusDot.title = 'Status: online';
} catch(err) {
// Connection failed or timed out
// If the page is not secure, try fallback to http if we used https for health check
if(!pageIsSecure && healthCheckUrl.startsWith('https:')){
try {
const fallback = healthCheckUrl.replace(/^https:/i, 'http:');
const controller2 = new AbortController();
const timeoutId2 = setTimeout(() => controller2.abort(), 5000);
await fetch(fallback, { method: 'HEAD', mode: 'no-cors', cache: 'no-cache', signal: controller2.signal });
clearTimeout(timeoutId2);
currentStatus = 'online';
statusDot.className = 'status-dot status-online';
statusDot.title = 'Status: online';
return;
} catch(fErr){
// continue to mark offline
}
}
currentStatus = 'offline';
statusDot.className = 'status-dot status-offline';
statusDot.title = 'Status: offline';
}
})();
} else if(manualStatus){
// Use manual status if health check is disabled
statusDot.className = `status-dot status-${manualStatus}`;
statusDot.title = `Status: ${manualStatus}`;
currentStatus = manualStatus;
a.appendChild(statusDot);
}
// Add info button if both hostname and port are available
if((hostAttr || host) && port){
const infoBtn = document.createElement('button');
infoBtn.className = 'info-btn';
infoBtn.title = 'Connection details';
infoBtn.innerHTML = 'ⓘ';
infoBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const displayHost = hostAttr || host;
const details = `Service: ${name}\nHost: ${displayHost}\nPort: ${port}\nProtocol: ${proto}`;
alert(details);
};
a.appendChild(infoBtn);
}
allServices.push(a);
return a;
}
// Export allServices for use by search and keyboard navigation
window.servicesData = { allServices, container };
}catch(err){
console.error(err);
container.innerHTML = '<p class="notes">Error loading services.xml — see console for details.</p>';
}
})();

172
js/themes.js Normal file
View File

@@ -0,0 +1,172 @@
/**
* themes.js - Theme management system
*/
const STORAGE_KEY_THEME = 'selected-theme';
const themes = {
dark: {
name: 'Dark (Default)',
primary: '#1a1a2e',
secondary: '#16213e',
accent: '#0f3460',
text: '#e4e4e4',
textMuted: '#a0a0a0',
border: '#2a2a3e',
cardBg: 'rgba(30, 30, 46, 0.7)',
headerBg: 'rgba(26, 26, 46, 0.85)',
overlayBg: 'rgba(60, 60, 60, 0.35)'
},
light: {
name: 'Light',
primary: '#f5f5f5',
secondary: '#ffffff',
accent: '#3498db',
text: '#2c3e50',
textMuted: '#7f8c8d',
border: '#dfe6e9',
cardBg: 'rgba(255, 255, 255, 0.85)',
headerBg: 'rgba(245, 245, 245, 0.95)',
overlayBg: 'rgba(255, 255, 255, 0.15)'
},
ocean: {
name: 'Ocean',
primary: '#0a1828',
secondary: '#1a2332',
accent: '#178582',
text: '#e0f4f4',
textMuted: '#8ab4b4',
border: '#1a3a3a',
cardBg: 'rgba(26, 35, 50, 0.75)',
headerBg: 'rgba(10, 24, 40, 0.9)',
overlayBg: 'rgba(23, 133, 130, 0.15)'
},
sunset: {
name: 'Sunset',
primary: '#1a0f1e',
secondary: '#2a1b2e',
accent: '#d4477a',
text: '#fce9e9',
textMuted: '#c4a4b4',
border: '#3a2a3e',
cardBg: 'rgba(42, 27, 46, 0.75)',
headerBg: 'rgba(26, 15, 30, 0.9)',
overlayBg: 'rgba(212, 71, 122, 0.15)'
},
forest: {
name: 'Forest',
primary: '#0f1a0f',
secondary: '#1a2a1a',
accent: '#4a9a4a',
text: '#e4f4e4',
textMuted: '#a4c4a4',
border: '#2a3a2a',
cardBg: 'rgba(26, 42, 26, 0.75)',
headerBg: 'rgba(15, 26, 15, 0.9)',
overlayBg: 'rgba(74, 154, 74, 0.15)'
}
};
// Apply theme
function applyTheme(themeName) {
const theme = themes[themeName] || themes.dark;
const root = document.documentElement;
root.style.setProperty('--color-primary', theme.primary);
root.style.setProperty('--color-secondary', theme.secondary);
root.style.setProperty('--color-accent', theme.accent);
root.style.setProperty('--color-text', theme.text);
root.style.setProperty('--color-text-muted', theme.textMuted);
root.style.setProperty('--color-border', theme.border);
root.style.setProperty('--color-card-bg', theme.cardBg);
root.style.setProperty('--color-header-bg', theme.headerBg);
root.style.setProperty('--color-overlay-bg', theme.overlayBg);
// Save preference
try {
localStorage.setItem(STORAGE_KEY_THEME, themeName);
} catch (e) {
console.error('Error saving theme:', e);
}
// Update active state in selector
document.querySelectorAll('.theme-option').forEach(option => {
option.classList.toggle('active', option.dataset.theme === themeName);
});
}
// Load saved theme
function loadSavedTheme() {
try {
const saved = localStorage.getItem(STORAGE_KEY_THEME);
return saved || 'dark';
} catch (e) {
console.error('Error loading theme:', e);
return 'dark';
}
}
// Create theme selector UI
function createThemeSelector() {
const selector = document.createElement('div');
selector.id = 'theme-selector';
selector.className = 'theme-selector';
const toggle = document.createElement('button');
toggle.className = 'theme-toggle';
toggle.innerHTML = '🎨';
toggle.title = 'Change Theme';
const menu = document.createElement('div');
menu.className = 'theme-menu';
Object.entries(themes).forEach(([key, theme]) => {
const option = document.createElement('div');
option.className = 'theme-option';
option.dataset.theme = key;
option.textContent = theme.name;
option.addEventListener('click', () => {
applyTheme(key);
menu.classList.remove('open');
});
menu.appendChild(option);
});
toggle.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.toggle('open');
});
// Close menu when clicking outside
document.addEventListener('click', () => {
menu.classList.remove('open');
});
selector.appendChild(toggle);
selector.appendChild(menu);
return selector;
}
// Initialize theme system
function initThemes() {
// Apply saved theme
const savedTheme = loadSavedTheme();
applyTheme(savedTheme);
// Add theme selector to header
const header = document.querySelector('header');
if (header) {
const selector = createThemeSelector();
header.appendChild(selector);
}
}
// Export for use in other modules
window.themesModule = {
init: initThemes,
applyTheme: applyTheme,
themes: themes
};

315
js/widgets.js Normal file
View File

@@ -0,0 +1,315 @@
/**
* widgets.js - Dashboard widgets (time, weather, etc.)
*/
const STORAGE_KEY_WIDGETS = 'enabled-widgets';
// Widget configurations
const widgetConfigs = {
clock: {
name: 'Clock',
icon: '🕐',
enabled: true
},
weather: {
name: 'Weather',
icon: '🌤️',
enabled: false,
apiKey: '', // User needs to set this
location: 'auto'
},
quote: {
name: 'Daily Quote',
icon: '💭',
enabled: false
}
};
// Load widget settings
function loadWidgetSettings() {
try {
const saved = localStorage.getItem(STORAGE_KEY_WIDGETS);
return saved ? JSON.parse(saved) : widgetConfigs;
} catch (e) {
console.error('Error loading widget settings:', e);
return widgetConfigs;
}
}
// Save widget settings
function saveWidgetSettings(settings) {
try {
localStorage.setItem(STORAGE_KEY_WIDGETS, JSON.stringify(settings));
} catch (e) {
console.error('Error saving widget settings:', e);
}
}
// Clock Widget
function createClockWidget() {
const widget = document.createElement('div');
widget.className = 'widget widget-clock';
const timeDisplay = document.createElement('div');
timeDisplay.className = 'widget-time';
const dateDisplay = document.createElement('div');
dateDisplay.className = 'widget-date';
function updateTime() {
const now = new Date();
// Time
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
timeDisplay.textContent = `${hours}:${minutes}:${seconds}`;
// Date
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
dateDisplay.textContent = now.toLocaleDateString('en-US', options);
}
updateTime();
setInterval(updateTime, 1000);
widget.appendChild(timeDisplay);
widget.appendChild(dateDisplay);
return widget;
}
// Weather Widget
function createWeatherWidget(settings) {
const widget = document.createElement('div');
widget.className = 'widget widget-weather';
const loading = document.createElement('div');
loading.className = 'widget-loading';
loading.textContent = '🌤️ Loading weather...';
widget.appendChild(loading);
// Check if API key is set
if (!settings.apiKey) {
widget.innerHTML = `
<div class="widget-error">
<p>🌤️ Weather widget requires an API key</p>
<small>Get a free key from <a href="https://openweathermap.org/api" target="_blank">OpenWeatherMap</a></small>
</div>
`;
return widget;
}
// Fetch weather data
let url = `https://api.openweathermap.org/data/2.5/weather?appid=${settings.apiKey}&units=metric`;
if (settings.location === 'auto') {
// Use geolocation
navigator.geolocation.getCurrentPosition(
(position) => {
url += `&lat=${position.coords.latitude}&lon=${position.coords.longitude}`;
fetchWeather(url, widget);
},
() => {
widget.innerHTML = '<div class="widget-error">Unable to get location</div>';
}
);
} else {
url += `&q=${settings.location}`;
fetchWeather(url, widget);
}
return widget;
}
function fetchWeather(url, widget) {
fetch(url)
.then(res => res.json())
.then(data => {
widget.innerHTML = `
<div class="weather-info">
<div class="weather-location">${data.name}</div>
<div class="weather-temp">${Math.round(data.main.temp)}°C</div>
<div class="weather-desc">${data.weather[0].description}</div>
<div class="weather-details">
<span>💨 ${data.wind.speed} m/s</span>
<span>💧 ${data.main.humidity}%</span>
</div>
</div>
`;
})
.catch(error => {
console.error('Weather fetch error:', error);
widget.innerHTML = '<div class="widget-error">Failed to load weather</div>';
});
}
// Quote Widget
function createQuoteWidget() {
const widget = document.createElement('div');
widget.className = 'widget widget-quote';
const loading = document.createElement('div');
loading.className = 'widget-loading';
loading.textContent = '💭 Loading quote...';
widget.appendChild(loading);
fetch('https://api.quotable.io/random')
.then(res => res.json())
.then(data => {
widget.innerHTML = `
<div class="quote-content">
<p class="quote-text">"${data.content}"</p>
<p class="quote-author">— ${data.author}</p>
</div>
`;
})
.catch(error => {
console.error('Quote fetch error:', error);
widget.innerHTML = '<div class="widget-error">Failed to load quote</div>';
});
return widget;
}
// Create widget container
function createWidgetContainer() {
const container = document.createElement('div');
container.id = 'widgets-container';
container.className = 'widgets-container';
return container;
}
// Initialize widgets
function initWidgets() {
const settings = loadWidgetSettings();
const container = createWidgetContainer();
// Add enabled widgets
if (settings.clock?.enabled) {
container.appendChild(createClockWidget());
}
if (settings.weather?.enabled) {
container.appendChild(createWeatherWidget(settings.weather));
}
if (settings.quote?.enabled) {
container.appendChild(createQuoteWidget());
}
// Add to header if any widgets are enabled
if (container.children.length > 0) {
const header = document.querySelector('header');
if (header) {
header.appendChild(container);
}
}
// Create widget settings button
createWidgetSettings();
}
// Create widget settings UI
function createWidgetSettings() {
const button = document.createElement('button');
button.className = 'widget-settings-btn control-btn';
button.innerHTML = '⚙️ Widgets';
button.title = 'Widget Settings';
button.addEventListener('click', showWidgetSettingsModal);
const header = document.querySelector('header');
if (header) {
let controls = header.querySelector('.import-export-controls');
if (!controls) {
controls = document.createElement('div');
controls.className = 'import-export-controls';
header.appendChild(controls);
}
controls.appendChild(button);
}
}
// Show widget settings modal
function showWidgetSettingsModal() {
const settings = loadWidgetSettings();
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>Widget Settings</h2>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="widget-setting">
<label>
<input type="checkbox" id="widget-clock" ${settings.clock?.enabled ? 'checked' : ''}>
🕐 Clock
</label>
</div>
<div class="widget-setting">
<label>
<input type="checkbox" id="widget-weather" ${settings.weather?.enabled ? 'checked' : ''}>
🌤️ Weather
</label>
<div class="widget-subsetting">
<input type="text" id="weather-api-key" placeholder="OpenWeatherMap API Key" value="${settings.weather?.apiKey || ''}">
<input type="text" id="weather-location" placeholder="Location (or 'auto')" value="${settings.weather?.location || 'auto'}">
</div>
</div>
<div class="widget-setting">
<label>
<input type="checkbox" id="widget-quote" ${settings.quote?.enabled ? 'checked' : ''}>
💭 Daily Quote
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn-save">Save & Reload</button>
<button class="btn-cancel">Cancel</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Event listeners
modal.querySelector('.modal-close').addEventListener('click', () => modal.remove());
modal.querySelector('.btn-cancel').addEventListener('click', () => modal.remove());
modal.querySelector('.btn-save').addEventListener('click', () => {
const newSettings = {
clock: {
...widgetConfigs.clock,
enabled: modal.querySelector('#widget-clock').checked
},
weather: {
...widgetConfigs.weather,
enabled: modal.querySelector('#widget-weather').checked,
apiKey: modal.querySelector('#weather-api-key').value,
location: modal.querySelector('#weather-location').value || 'auto'
},
quote: {
...widgetConfigs.quote,
enabled: modal.querySelector('#widget-quote').checked
}
};
saveWidgetSettings(newSettings);
modal.remove();
location.reload();
});
// Close on background click
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
}
// Export for use in other modules
window.widgetsModule = {
init: initWidgets
};

1
logos/bazarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Subtitle Edit</title><path d="M3.858.02C1.744.23.16 1.91.02 4.09c-.038.598-.02 15.896.02 16.156.3 1.996 1.752 3.455 3.7 3.719.418.057 16.38.04 16.674-.018 1.433-.28 2.614-1.164 3.156-2.363.2-.443.304-.776.377-1.208.047-.282.075-16.036.029-16.509A4.266 4.266 0 0 0 20.348.048C20.065.008 4.261-.02 3.858.02m7.237 6.15c.707.707 1.285 1.299 1.285 1.315 0 .024-.57.03-2.79.03-3.106 0-2.95-.008-3.286.16-1.145.58-1.175 2.2-.052 2.8.34.18.265.174 1.725.192 1.404.018 1.475.023 1.976.153 1.495.388 2.688 1.64 3.015 3.164a4.2 4.2 0 0 1-3.547 5.057c-.347.046-6.605.05-6.605.004 0-.016.573-.602 1.273-1.302L5.36 16.47l1.87-.01c2.07-.009 1.97-.002 2.326-.172a1.566 1.566 0 0 0 .421-2.532c-.431-.43-.571-.461-2.05-.462-1.802 0-2.364-.125-3.253-.721-3.078-2.066-2.152-6.837 1.475-7.597.38-.08.522-.086 2.11-.089l1.551-.003 1.284 1.285m10.067-1.256c0 .017-.578.608-1.284 1.315l-1.284 1.286h-4.427l-1.296-1.298a68.614 68.608 0 0 1-1.296-1.315c0-.01 2.157-.018 4.793-.018 3.813 0 4.794.006 4.794.03m-2.562 7.06-.006 1.308h-4.449l-.033-.094c-.336-.942-.695-1.527-1.346-2.194a4.325 4.325 0 0 1-.292-.313c0-.01 1.38-.016 3.066-.016h3.066l-.006 1.309m1.278 5.78a67.498 67.492 0 0 1 1.284 1.302c0 .01-1.955.018-4.344.018-2.389 0-4.344-.008-4.344-.018 0-.01.103-.12.228-.243a5.453 5.453 0 0 0 1.38-2.185l.053-.16h4.458l1.285 1.285"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
logos/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

8
logos/icon-192.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg width="192" height="192" xmlns="http://www.w3.org/2000/svg">
<rect width="192" height="192" fill="#0f3460"/>
<circle cx="96" cy="96" r="70" fill="#26fdd9" opacity="0.3"/>
<circle cx="96" cy="96" r="50" fill="#2bb2e6" opacity="0.5"/>
<path d="M 96 56 L 116 86 L 76 86 Z" fill="#fff"/>
<path d="M 96 136 L 76 106 L 116 106 Z" fill="#fff"/>
<circle cx="96" cy="96" r="8" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 406 B

BIN
logos/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

1
logos/lidarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>MusicBrainz</title><path d="M11.582 0L1.418 5.832v12.336L11.582 24V10.01L7.1 12.668v3.664c.01.111.01.225 0 .336-.103.435-.54.804-1 1.111-.802.537-1.752.509-2.166-.111-.413-.62-.141-1.631.666-2.168.384-.28.863-.399 1.334-.332V6.619c0-.154.134-.252.226-.308L11.582 3zm.836 0v6.162c.574.03 1.14.16 1.668.387a2.225 2.225 0 0 0 1.656-.717 1.02 1.02 0 1 1 1.832-.803l.004.006a1.022 1.022 0 0 1-1.295 1.197c-.34.403-.792.698-1.297.85.34.263.641.576.891.928a1.04 1.04 0 0 1 .777.125c.768.486.568 1.657-.318 1.857-.886.2-1.574-.77-1.09-1.539.02-.03.042-.06.065-.09a3.598 3.598 0 0 0-1.436-1.166 4.142 4.142 0 0 0-1.457-.369v4.01c.855.06 1.256.493 1.555.834.227.256.356.39.578.402.323.018.568.008.806 0a5.44 5.44 0 0 1 .895.022c.94-.017 1.272-.226 1.605-.446a2.533 2.533 0 0 1 1.131-.463 1.027 1.027 0 0 1 .12-.263 1.04 1.04 0 0 1 .105-.137c.023-.025.047-.044.07-.066a4.775 4.775 0 0 1 0-2.405l-.012-.01a1.02 1.02 0 1 1 .692.272h-.057a4.288 4.288 0 0 0 0 1.877h.063a1.02 1.02 0 1 1-.545 1.883l-.047-.033a1 1 0 0 1-.352-.442 1.885 1.885 0 0 0-.814.354 3.03 3.03 0 0 1-.703.365c.757.555 1.772 1.6 2.199 2.299a1.03 1.03 0 0 1 .256-.033 1.02 1.02 0 1 1-.545 1.88l-.047-.03a1.017 1.017 0 0 1-.27-1.376.72.72 0 0 1 .051-.072c-.445-.775-2.026-2.28-2.46-2.387a4.037 4.037 0 0 0-1.31-.117c-.24.008-.513.018-.866 0-.515-.027-.783-.333-1.043-.629-.26-.296-.51-.56-1.055-.611V18.5a1.877 1.877 0 0 0 .426-.135.333.333 0 0 1 .058-.027c.56-.267 1.421-.91 2.096-2.447a1.02 1.02 0 0 1-.27-1.344 1.02 1.02 0 1 1 .915 1.54 6.273 6.273 0 0 1-1.432 2.136 1.785 1.785 0 0 1 .691.306.667.667 0 0 0 .37.168 3.31 3.31 0 0 0 .888-.222 1.02 1.02 0 0 1 1.787-.79v-.005a1.02 1.02 0 0 1-.773 1.683 1.022 1.022 0 0 1-.719-.287 3.935 3.935 0 0 1-1.168.287h-.05a1.313 1.313 0 0 1-.71-.275c-.262-.177-.51-.345-1.402-.12a2.098 2.098 0 0 1-.707.2V24l10.164-5.832V5.832zm4.154 4.904a.352.352 0 0 0-.197.639l.018.01c.163.1.378.053.484-.108v-.002a.352.352 0 0 0-.303-.539zm-4.99 1.928L7.082 9.5v2l4.5-2.668zm8.385.38a.352.352 0 0 0-.295.165v.002a.35.35 0 0 0 .096.473l.013.01a.357.357 0 0 0 .487-.108.352.352 0 0 0-.301-.541zM16.09 8.647a.352.352 0 0 0-.277.163.355.355 0 0 0 .296.54c.482 0 .463-.73-.02-.703zm3.877 2.477a.352.352 0 0 0-.295.164.35.35 0 0 0 .094.475l.015.01a.357.357 0 0 0 .485-.11.352.352 0 0 0-.3-.539zm-4.375 3.594a.352.352 0 0 0-.291.172.35.35 0 0 0-.04.265.352.352 0 1 0 .33-.437zm4.375.789a.352.352 0 0 0-.295.164v.002a.352.352 0 0 0 .094.473l.015.01a.357.357 0 0 0 .485-.108.352.352 0 0 0-.3-.54zm-2.803 2.488v.002a.347.347 0 0 0-.223.084.352.352 0 0 0 .23.62.347.347 0 0 0 .23-.085.348.348 0 0 0 .12-.24.353.353 0 0 0-.35-.38.347.347 0 0 0-.007 0Z"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

1
logos/openwebui.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
logos/prowlarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenSearch</title><path d="M23.1515 8.8125a.8484.8484 0 0 0-.8484.8485c0 6.982-5.6601 12.6421-12.6421 12.6421a.8485.8485 0 0 0 0 1.6969C17.5802 24 24 17.5802 24 9.661a.8485.8485 0 0 0-.8485-.8485Zm-5.121 5.4375c.816-1.3311 1.6051-3.1058 1.4498-5.5905-.3216-5.1468-4.9832-9.0512-9.3851-8.6281C8.372.1971 6.6025 1.6017 6.7598 4.1177c.0683 1.0934.6034 1.7386 1.473 2.2348.8279.4722 1.8914.7713 3.097 1.1104 1.4563.4096 3.1455.8697 4.4438 1.8265 1.5561 1.1467 2.6198 2.4759 2.2569 4.9606Zm-16.561-9C.6535 6.581-.1355 8.3558.0197 10.8405c.3216 5.1468 4.9832 9.0512 9.385 8.6281 1.7233-.1657 3.4927-1.5703 3.3355-4.0863-.0683-1.0934-.6034-1.7386-1.4731-2.2348-.8278-.4722-1.8913-.7713-3.0969-1.1104-1.4563-.4096-3.1455-.8697-4.4438-1.8265-1.5561-1.1467-2.6198-2.476-2.257-4.9606Z"/></svg>

After

Width:  |  Height:  |  Size: 860 B

1
logos/radarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>radarr</title><path d="M5.274 0C3.189.039 1.19 1.547 1.19 4.705l.184 14.518c0 1.47 1.103 2.205 2.573 2.021L3.764 3.786c0-1.654.919-1.838 2.022-1.103l14.7 8.27c1.103.734 1.655 1.47 1.838 2.756.92-1.654.552-4.043-1.286-5.33L7.991.846A4.559 4.559 0 0 0 5.274.001zm1.982 6.91-.184 10.107 9.004-5.146Zm13.598 6.064-15.068 8.82c-.92.552-2.022.736-3.124.368.918 1.47 3.307 2.389 5.145 1.47l12.68-7.35c1.102-.736 1.286-2.022.367-3.308z"/></svg>

After

Width:  |  Height:  |  Size: 514 B

1
logos/readarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>BookStack</title><path d="M.3013 17.6146c-.1299-.3387-.5228-1.5119-.1337-2.4314l9.8273 5.6738a.329.329 0 0 0 .3299 0L24 12.9616v2.3542l-13.8401 7.9906-9.8586-5.6918zM.1911 8.9628c-.2882.8769.0149 2.0581.1236 2.4261l9.8452 5.6841L24 9.0823V6.7275L10.3248 14.623a.329.329 0 0 1-.3299 0L.1911 8.9628zm13.1698-1.9361c-.1819.1113-.4394.0015-.4852-.2064l-.2805-1.1336-2.1254-.1752a.33.33 0 0 1-.1378-.6145l5.5782-3.2207-1.7021-.9826L.6979 8.4935l9.462 5.463 13.5104-7.8004-4.401-2.5407-5.9084 3.4113zm-.1821-1.7286.2321.938 5.1984-3.0014-2.0395-1.1775-4.994 2.8834 1.3099.108a.3302.3302 0 0 1 .2931.2495zM24 9.845l-13.6752 7.8954a.329.329 0 0 1-.3299 0L.1678 12.0667c-.3891.919.003 2.0914.1332 2.4311l9.8589 5.692L24 12.1993V9.845z"/></svg>

After

Width:  |  Height:  |  Size: 812 B

1
logos/scrutiny.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>smart</title><path d="M10.85.846A11.138 11.138 0 0 0 0 11.979v.04a11.136 11.136 0 0 0 10.844 11.135h.283a10.983 10.983 0 0 0 4.041-.758.395.395 0 0 0 .256-.369v-5.564a.21.21 0 0 0-.274-.195c-1.202.489-2.215.957-3.96.957a5.222 5.222 0 0 1-5.22-5.22 5.22 5.22 0 0 1 5.22-5.22c1.745 0 2.758.467 3.96.955a.21.21 0 0 0 .274-.193V1.979a.395.395 0 0 0-.256-.37 10.983 10.983 0 0 0-4.037-.763Zm5.863 1.82v18.67a.238.238 0 0 0 .377.19c3.413-2.122 6.91-8.16 6.91-9.52 0-1.36-3.497-7.396-6.91-9.522a.238.238 0 0 0-.377.182Z"/></svg>

After

Width:  |  Height:  |  Size: 599 B

1
logos/simple-icons Submodule

Submodule logos/simple-icons added at 786fc0dc7c

View File

@@ -0,0 +1,18 @@
{
"build": {
"dockerfile": "../Dockerfile",
"context": "..",
"target": "base"
},
"postCreateCommand": "npm ci --no-audit --no-fund",
"customizations": {
"vscode": {
"extensions": [
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"samverschueren.linter-xo",
"davidanson.vscode-markdownlint"
]
}
}
}

View File

@@ -0,0 +1,11 @@
# NodeJS
node_modules/
npm-debug.log
# Build files
/index.js
/index-icons.js
/index.mjs
/index-icons.mjs
/index.d.ts
/sdk.js

View File

@@ -0,0 +1,20 @@
root=true
[*]
charset=utf-8
indent_style=tab
insert_final_newline=true
trim_trailing_whitespace=true
[*.md]
indent_style = space
indent_size=unset
[*.svg]
insert_final_newline=false
[*.yml]
indent_style=space
[.github/**/*.md]
trim_trailing_whitespace=false # Templates with trailing whitespace are more usable

View File

@@ -0,0 +1,13 @@
* text=auto
# SVGs are treated as binary by default
*.svg text
# Don't diff machine generated files
slugs.md -diff
# Don't diff package lock files
package-lock.json -diff
# Don't export/archive these files
.github export-ignore

View File

@@ -0,0 +1,4 @@
contact_links:
- name: Website
about: Issues and improvements for the website
url: https://github.com/simple-icons/simple-icons-website-rs/issues/new?template=website.md

View File

@@ -0,0 +1,34 @@
name: Documentation
description: Report problems and suggest ideas for the documentation
labels: [docs]
body:
- type: markdown
attributes:
value: |
Before opening a new issue, make sure it isn't covered by an existing issue.
Please search for [issues with the `docs` label][docs-issues] (including
closed issues) before you continue.
[docs-issues]: https://github.com/simple-icons/simple-icons/labels/docs
- type: dropdown
attributes:
label: Kind of Issue
description: |
If your issue type is not here, select "other" and explain in the
"Description" field below.
options: [Improvement, Mistake, Other]
validations:
required: true
- type: textarea
attributes:
label: Description
description: |
Include as much detail as possible, for example:
- A (perma)link to the docs in question
- Suggestions for how to change the docs
placeholder: "Example: The documentation doesn't cover my use case of the NPM package..."
validations:
required: true

View File

@@ -0,0 +1,35 @@
name: Icon removal
description: Report an icon for removal
labels: [breaking change]
title: 'Removal: <brand-name>'
body:
- type: markdown
attributes:
value: |
Before opening a new issue, make sure it isn't covered by an existing issue.
Please search for [issues with the `breaking change` label][breaking-issues]
(including closed issues) before you continue. If you find one for the brand
you're reporting then leave a comment on it or add a reaction.
[breaking-issues]: https://github.com/simple-icons/simple-icons/labels/breaking%20change
- type: input
attributes:
label: Brand Name
placeholder: 'Example: Simple Icons'
validations:
required: true
- type: textarea
attributes:
label: Reason for Removal
description: |
- If the brand no longer exists, include a link supporting that claim.
- If the brand should be removed for another reason, explain why.
placeholder: >-
Example: [this blogpost](https://example.com) announced the brand has been discontinued...
validations:
required: true

View File

@@ -0,0 +1,95 @@
name: Icon request
description: Request a new icon for Simple Icons
labels: [new icon, permissions in review]
title: 'Request: <brand-name>'
body:
- type: markdown
attributes:
value: |
We won't add non-brand icons or anything related to illegal services. If in
doubt, open an issue and we'll have a look. For more details see the
[Contributing Guidelines].
Before opening a new issue, make sure it isn't covered by an existing issue.
Please search for [issues with the `new icon` label][new-icon-issues]
(including closed issues) before you continue. If you find one for the brand
you're requesting then leave a comment on it or add a reaction.
> [!TIP]
> In order to view website rankings on `traffic.cv`:
> Go to `https://traffic.cv/website`, enter the website address, and click on Analyze Traffic.
[contributing guidelines]: https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md
[new-icon-issues]: https://github.com/simple-icons/simple-icons/issues?q=is%3Aissue+label%3A%22new+icon%22
- type: input
attributes:
label: Brand Name
placeholder: 'Example: Simple Icons'
validations:
required: true
- type: input
attributes:
label: Website
description: |
For non-web brands you can add a relevant link. You can put "None" if you don't
think there's a website.
placeholder: 'Example: https://simpleicons.org'
validations:
required: true
- type: textarea
attributes:
label: Popularity Metric
description: |
Provide either a [Traffic.cv rank], which must be in the top 500,000 to qualify,
or failing that another metric from [our contributing guidelines] that we can
use to assess the popularity of the requested brand.
[Traffic.cv rank]: https://traffic.cv
[our contributing guidelines]: https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#assessing-popularity
placeholder: 'Example: The Traffic.cv rank is 261,758. See https://traffic.cv/simpleicons.org'
validations:
required: true
- type: checkboxes
attributes:
label: Forbidden Brands
options:
- label: I have reviewed the list of [forbidden brands](https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#forbidden-brands) and can confirm the brand I am requesting is not one of them, nor is it a subsidiary of one of them.
required: true
- type: input
attributes:
label: Terms of Service
description: |
A lot of websites specify whether or not we can use their brand assets in their Terms of Service.
Please ensure you include the link here where relevant.
validations:
required: true
- type: textarea
attributes:
label: Official Resources for Icon and Color
description: |
Media kits, brand guidelines, SVG files, etc. You can set
this to "None" if you are unable to find any resources.
placeholder: |
Example:
- SVG: https://github.com/simple-icons/simple-icons/blob/develop/icons/simpleicons.svg
- Color: `#111111`, from the website header
validations:
required: true
- type: textarea
attributes:
label: Additional Comments
description: |
Is there anything else we should know about the brand? Remember that not everyone knows the brand as well as you do. For example:
- Are there multiple options for the logo and/or color?
- Is the icon released under a license?
- If you think the brand might not be accepted, why do you think it should be considered?
placeholder: 'Example: There are two variants of this icon...'

View File

@@ -0,0 +1,53 @@
name: Icon update
description: Help us improve by reporting outdated icons
labels: [update icon/data, permissions in review]
title: 'Update: <brand-name>'
body:
- type: markdown
attributes:
value: |
Before opening a new issue, make sure it isn't covered by an existing issue.
Please search for [issues with the `update icon/data` label][update-icon-data-issues]
(including closed issues) before you continue. If you find one for the brand
you're reporting then leave a comment on it or add a reaction.
[update-icon-data-issues]: https://github.com/simple-icons/simple-icons/labels/update%20icon/data
- type: input
attributes:
label: Brand Name
placeholder: 'Example: Simple Icons'
validations:
required: true
- type: input
attributes:
label: Terms of Service
description: |
A lot of websites specify whether or not we can use their brand assets in their Terms of Service.
Please ensure you include the link here where relevant.
validations:
required: true
- type: textarea
attributes:
label: Official Resources for Icon and Color
description: |
Media kits, brand guidelines, SVG files, etc. You can set
this to "None" if you are unable to find any resources.
placeholder: |
Example:
svg: https://github.com/simple-icons/simple-icons/blob/develop/icons/simpleicons.svg
color: `#111111`, from the website header
validations:
required: true
- type: textarea
attributes:
label: Additional Comments
description: |
Is there anything else we should know about the brand? Remember that not everyone knows the brand as well as you do. For example:
- Are there multiple options for the logo and/or color?
- Is the icon released under a license?
placeholder: 'Example: There are two variants of this icon...'

View File

@@ -0,0 +1,63 @@
name: Packages
description: Report problems and suggest ideas for the NPM and Packagist packages
labels: [package]
body:
- type: markdown
attributes:
value: |
Before opening a new issue, make sure it isn't covered by an existing issue.
Please search for [issues with the `package` label][package-issues] (including
closed issues) before you continue.
[package-issues]: https://github.com/simple-icons/simple-icons/labels/package
- type: dropdown
attributes:
label: Kind of Issue
description: |
If your issue type is not here, select "other" and explain in the
"Description" field below.
options: [Bug, Feature, Performance, Other]
validations:
required: true
- type: dropdown
attributes:
label: This issue concerns the...
options: [NPM package, Packagist package]
multiple: true
validations:
required: true
- type: input
attributes:
label: Package Version
placeholder: 'Example: 5.11.0'
validations:
required: true
- type: textarea
attributes:
label: Other Software
description: |
The software that you are using the package with (Node.js & NPM, PHP & Packagist,
Browser(s), other) and their versions. You can put "None" if you are unsure.
placeholder: |
Example:
Node.js v16
Chrome 92.0.4515.159 (Official Build) (64-bit)
validations:
required: true
- type: textarea
attributes:
label: Description
description: |
Anything relevant, for example:
- For bugs: "Steps to reproduce" and "Expected behavior"
- For feature requests: An example of a use case
- For performance: An example where performance is poor
placeholder: 'Example: The NPM package does not work for my use case...'
validations:
required: true

View File

@@ -0,0 +1,39 @@
<!--
Before opening your pull request, have a quick look at our contribution guidelines:
https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md
Consider adding a preview image of your submission using:
https://simpleicons.org/preview
-->
**Issue:** closes #
**Popularity metric:**
<!--
Regardless of whether or not the linked issue (if there is one) has a metric, please include the metric here for PR reviewers to validate. See our contributing guidelines at https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#assessing-popularity for more details on how we assess a brand's popularity.
-->
**Terms of Service link:**
<!--
As part of the checklist below, you acknowledge you have reviewed the terms of service of a brand, to ensure we are granted permission to include this brand. Please link here to the terms you have reviewed, to make maintainer review easier. Ideally link to a section and/or paragraph.
-->
### Checklist
- [ ] I have reviewed the [forbidden brands](https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#forbidden-brands) list and confirm the brand I am submitting a PR for is not one of them, nor is it a subsidiary of one of those brands
- [ ] I have reviewed the brand's terms of service, and am confident we can add this icon
- [ ] I updated the JSON data in `data/simple-icons.json`
- [ ] I optimized the icon with SVGO or SVGOMG
- [ ] The SVG `viewbox` is `0 0 24 24`
### Description
<!--
Anything relevant, for example:
- Why did you pick the hex value?
- Did you manually vectorize the logo?
- Have you used multiple sources?
- etc.
-->

View File

@@ -0,0 +1,31 @@
# When working in a fork, some workflows should not be executed
# as they do not have the necessary permissions. This causes
# them to fail and only serve to generate noise for people
# watching the fork's activity.
name: Check if running in a fork
description: Check if a workflow is running in a forked repository and set an output accordingly.
inputs:
in-fork-message:
description: Message to display when the workflow is running in a fork.
required: true
outputs:
is-fork:
description: Indicates if the current repository is a fork.
value: ${{ steps.check-is-fork.outputs.is-fork }}
runs:
using: composite
steps:
- id: check-is-fork
shell: bash
env:
IN_FORK_MESSAGE: ${{ inputs.in-fork-message }}
run: |
if [ "${{ github.repository_owner }}" != "simple-icons" ]; then
echo "is-fork=true" >> $GITHUB_OUTPUT
echo "$IN_FORK_MESSAGE"
else
echo "is-fork=false" >> $GITHUB_OUTPUT
fi

View File

@@ -0,0 +1,25 @@
name: Get issue/pull request labels
description: Get the current labels of an issue or pull request
inputs:
issue_number:
description: Issue or pull request number to get labels from
required: true
github-token:
description: GitHub token used to authenticate with the GitHub API
required: true
outputs:
labels:
description: Labels of the issue or pull request
value: ${{ steps.get-labels.outputs.labels }}
runs:
using: composite
steps:
- id: get-labels
shell: sh
env:
GH_TOKEN: ${{ inputs.github-token }}
run: |
labels="$(gh api 'repos/simple-icons/simple-icons/issues/${{ inputs.issue_number }}' --jq '.labels.[].name' | tr '\n' ',')"
echo "labels=$labels" >> $GITHUB_OUTPUT

View File

@@ -0,0 +1,16 @@
name: Get version
description: Get the current version of the project
outputs:
version:
description: The version of the project
value: ${{ steps.get-version.outputs.version }}
runs:
using: composite
steps:
- id: get-version
shell: sh
run: |
version="$(grep version -m 1 -i package.json | cut -d '"' -f4)"
echo "version=$version" >> $GITHUB_OUTPUT

View File

@@ -0,0 +1,38 @@
new icon:
- any: [icons/*.svg]
status: added
update icon/data:
- any: [icons/*.svg]
status: modified
- all: [data/simple-icons.json]
status: modified
breaking change:
- any: [icons/*.svg]
status: removed
docs:
- any: ['*.md']
status: modified
meta:
- any:
[
'.github/*',
'scripts/*',
'tests/*',
'.husky/*',
'*.mjs',
'*.d.ts',
'*.json',
'*.toml',
'*',
]
status: modified
dependencies:
- all: ['package-lock.json']
status: modified
permissions in review:
- any: [icons/*.svg]
status: added
- any: [icons/*.svg]
status: modified
- all: [data/simple-icons.json]
status: modified

View File

@@ -0,0 +1,27 @@
{
repositories: ['simple-icons/simple-icons'],
extends: [
'config:recommended',
// Make sure we get a single PR combining all updates
'group:all',
],
// Disable Dependency Dashboard
dependencyDashboard: false,
// Use our labelling system
labels: ['dependencies'],
// We generally always want the major version
separateMajorMinor: false,
// We manually update digest dependencies (eg. hashes in GitHub Actions)
digest: {enabled: false},
// PR title and commit message when updating dependencies
commitMessage: 'Update dependencies',
// Simplify PR body when updating dependencies
prBodyTemplate: '{{{header}}}{{{table}}}{{{warnings}}}{{{notes}}}{{{changelogs}}}',
}

View File

@@ -0,0 +1,160 @@
name: Add Pull Request Labels and Assign to Project
on:
pull_request_target:
types: [opened, closed]
jobs:
add-labels:
permissions:
contents: read
pull-requests: write
issues: write
runs-on: ubuntu-latest
if: |
github.event.action == 'opened' &&
github.event.pull_request.base.ref != 'master'
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- uses: simple-icons/labeler@v1
with:
repo-token: ${{ steps.app-token.outputs.token }}
# TODO: The next job has been temporarily disabled until the maintainers
# team decide to use projects board again.
# assign-to-project:
# runs-on: ubuntu-latest
# name: Assign to Project
# if: |
# github.event.action == 'opened' &&
# github.event.pull_request.base.ref != 'master'
# needs: add-labels
# steps:
# - uses: actions/create-github-app-token@v1
# id: app-token
# with:
# app-id: ${{ vars.BOT_APP_ID }}
# private-key: ${{ secrets.BOT_PRIVATE_KEY }}
# - name: Checkout
# uses: actions/checkout@v5
# - id: get-labels
# uses: ./.github/actions/get-labels
# with:
# issue_number: ${{ github.event.pull_request.number }}
# github-token: ${{ secrets.GITHUB_TOKEN }}
# - id: get-si-members
# name: Get simple-icons members
# run: |
# members="$(curl -H 'Authorization: Bearer ${{ steps.app-token.outputs.token }}' --retry 5 -s https://api.github.com/orgs/simple-icons/members | jq .[].login | tr '\n' ',' | sed -e 's/"//g' -e 's/,$//')"
# echo "members=$members" >> $GITHUB_OUTPUT
# - id: get-linked-issues
# name: Get linked issue numbers
# uses: mondeja/pr-linked-issues-action@v2
# with:
# # Lazy linked issues. If one of the lines of the pull request body
# # matches one of the next contents, the matching issue number will
# # be added to `issues` output:
# add_links_by_content: |
# **Issue:** #{issue_number}
# **Issue**: #{issue_number}
# **Close:** #{issue_number}
# **Close**: #{issue_number}
# **Closes:** #{issue_number}
# **Closes**: #{issue_number}
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - id: priority-1
# name: Assign `update icon/data` pull requests to "Priority 1"
# uses: srggrs/assign-one-project-github-action@1.3.1
# env:
# MY_GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# if: contains(steps.get-labels.outputs.labels, 'update icon/data')
# with:
# project: https://github.com/orgs/simple-icons/projects/2
# column_name: Priority 1
# - id: priority-2
# name: Assign `new icon` pull requests to "Priority 2"
# uses: srggrs/assign-one-project-github-action@1.3.1
# env:
# MY_GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# # the PR has the `new icon` label along with a linked issue and
# # the opener is not a member of simple-icons organization
# if: |
# contains(steps.get-labels.outputs.labels, 'new icon') &&
# join(steps.get-linked-issues.outputs.issues) != '' &&
# contains(steps.get-si-members.outputs.members, github.event.pull_request.user.login) == false
# with:
# project: https://github.com/orgs/simple-icons/projects/2
# column_name: Priority 2
# - id: priority-3
# name: Assign `new icon` pull requests by maintainers to "Priority 3"
# uses: srggrs/assign-one-project-github-action@1.3.1
# env:
# MY_GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# # the PR has the `new icon` label along with a linked issue and
# # the opener is a member of the simple-icons organisation
# if: |
# contains(steps.get-labels.outputs.labels, 'new icon') &&
# join(steps.get-linked-issues.outputs.issues) != '' &&
# contains(steps.get-si-members.outputs.members, github.event.pull_request.user.login) == true
# with:
# project: https://github.com/orgs/simple-icons/projects/2
# column_name: Priority 3
# - id: priority-4
# name: Assign `new icon` pull requests by maintainers without an issue to "Priority 4"
# uses: srggrs/assign-one-project-github-action@1.3.1
# env:
# MY_GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# # the PR has the `new icon` label but with no linked issue and
# # the opener is a member of the simple-icons organisation
# if: |
# contains(steps.get-labels.outputs.labels, 'new icon') &&
# join(steps.get-linked-issues.outputs.issues) == '' &&
# contains(steps.get-si-members.outputs.members, github.event.pull_request.user.login) == true
# with:
# project: https://github.com/orgs/simple-icons/projects/2
# column_name: Priority 4
# - name: Assign pull requests to "Unprioritised"
# uses: srggrs/assign-one-project-github-action@1.3.1
# env:
# MY_GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# if: |
# steps.priority-1.conclusion == 'skipped' &&
# steps.priority-2.conclusion == 'skipped' &&
# steps.priority-3.conclusion == 'skipped' &&
# steps.priority-4.conclusion == 'skipped'
# with:
# project: https://github.com/orgs/simple-icons/projects/2
# column_name: Unprioritised
# unassign-from-project:
# runs-on: ubuntu-latest
# name: Unassign from Project
# if: |
# github.event.action != 'opened' &&
# github.event.pull_request.merged == false &&
# github.event.pull_request.base.ref != 'master'
# steps:
# - uses: actions/create-github-app-token@v1
# id: app-token
# with:
# app-id: ${{ vars.BOT_APP_ID }}
# private-key: ${{ secrets.BOT_PRIVATE_KEY }}
# - name: Assign closed pull requests to "Completed or Abandoned"
# uses: srggrs/assign-one-project-github-action@1.3.1
# env:
# MY_GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# with:
# project: https://github.com/orgs/simple-icons/projects/2
# column_name: Completed or Abandoned

View File

@@ -0,0 +1,23 @@
name: Autocloser
on:
issues:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v5
- name: Autoclose Issues
run: node scripts/autoclose-issues/autoclose.app.js
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
ISSUE_NUMBER: ${{ github.event.issue.number }}

View File

@@ -0,0 +1,101 @@
name: Create Release Pull Request
on:
# THIS WORKFLOW SHOULD NEVER BE TRIGGERED ON A PUSH EVENT. IF TRIGGERED ON A
# PUSH EVENT IT MAY CREATE AN ENDLESS STREAM OF 'version bump' COMMITS.
workflow_dispatch:
schedule:
# "At 00:00 on Sunday" (https://crontab.guru/once-a-week)
- cron: '0 0 * * 0'
# This Workflow can be triggered manually through the GitHub UI or API. For the
# API use the following request:
# curl -X POST \
# -H "Authorization: Bearer <token>" \
# -d '{"ref":"develop"}' \
# https://api.github.com/repos/simple-icons/simple-icons/actions/workflows/create-release.yml/dispatches
# Replacing <token> by a personal access token with scope `public_repo`
jobs:
check-is-fork:
name: Check if running in a fork
runs-on: ubuntu-latest
permissions: {}
outputs:
is-fork: ${{ steps.check.outputs.is-fork }}
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/check-is-fork
id: check
with:
in-fork-message: 'Create Release Pull Request workflow only can run in the main repository, skipping.'
release-pr:
runs-on: ubuntu-latest
needs: check-is-fork
permissions:
contents: read
pull-requests: write
issues: write
if: |
github.event_name != 'push' &&
needs.check-is-fork.outputs.is-fork != 'true'
outputs:
did-create-pr: ${{ steps.release.outputs.did-create-pr }}
new-version: ${{ steps.release.outputs.new-version }}
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- uses: simple-icons/release-action@v3
id: release
with:
repo-token: ${{ steps.app-token.outputs.token }}
version-bump:
runs-on: ubuntu-latest
needs: release-pr
permissions:
contents: write
if: |
github.event_name != 'push' &&
needs.release-pr.outputs.did-create-pr == 'true'
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v5
with:
# Ensure the commit can be pushed regardless of branch protections (must belong to an admin of this repo)
token: ${{ steps.app-token.outputs.token }}
# Ensure we are checked out on the develop branch
ref: develop
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: npm
cache-dependency-path: '**/package-lock.json'
- name: Bump version
run: |
npm version --no-commit-hooks --no-git-tag-version \
"${{ needs.release-pr.outputs.new-version }}"
- name: Install dependencies
run: npm ci --no-audit --no-fund
- name: Update major version in CDN URLs
run: node scripts/release/update-cdn-urls.js
- name: Update SVGs count milestone
run: node scripts/release/update-svgs-count.js
- name: Update slugs table
run: node scripts/release/update-slugs-table.js
- name: Update SDK Typescript definitions
run: node scripts/release/update-sdk-ts-defs.js
- name: Commit version bump
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: Bump version
commit_user_name: 'simple-icons[bot]'
commit_user_email: 'simple-icons[bot]@users.noreply.github.com'
commit_author: 'simple-icons[bot] <simple-icons[bot]@users.noreply.github.com>'

View File

@@ -0,0 +1,20 @@
name: Merge Release Pull Request
on:
pull_request_review:
types: [submitted]
jobs:
release:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'release')
permissions:
pull-requests: write
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- uses: simple-icons/release-action@v3
with:
repo-token: ${{ steps.app-token.outputs.token }}

View File

@@ -0,0 +1,149 @@
name: Publish Releases
on:
push:
branches:
- master
jobs:
sanity-check:
name: Pre-publish checks
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: npm
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
run: npm ci --no-audit --no-fund
- name: Run linters
run: npm run lint
- name: Build NodeJS package
run: npm run build
- name: Run tests
run: npm run test
npm:
name: NPM Package
needs: sanity-check
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # for OIDC authentication
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: npm
cache-dependency-path: '**/package-lock.json'
- id: get-version
uses: ./.github/actions/get-version
- name: Install dependencies
run: npm ci --no-audit --no-fund
- name: Reformat to regular markdown
run: node scripts/release/reformat-markdown.js "${{ steps.get-version.outputs.version }}"
- name: Update SDK Typescript definitions
run: node scripts/release/update-sdk-ts-defs.js
- name: Minify icons data file
run: node scripts/release/minify-icons-data.js
- name: Build NodeJS package
run: npm run build
- name: Deploy to NPM
run: npm publish
github:
name: GitHub release
needs: sanity-check
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v5
- name: Get commit message (for release title and body)
id: commit
uses: kceb/git-message-action@v3
- id: get-version
uses: ./.github/actions/get-version
- name: Reformat to regular markdown
run: node scripts/release/reformat-markdown.js "${{ steps.get-version.outputs.version }}"
- name: Configure GIT credentials
run: |
git config user.name "simple-icons[bot]"
git config user.email "simple-icons[bot]@users.noreply.github.com"
# Commit that will only be included in the tag
- name: Commit CDN theme image links removal
run: |
git add README.md
git commit -m 'Replace README CDN theme image links'
- name: Create and push git tag
run: |
set -e
tag="${{ steps.get-version.outputs.version }}"
git tag -a "${tag}" -m "$GIT_MESSAGE"
git push origin "${tag}"
env:
GIT_MESSAGE: ${{ steps.commit.outputs.git-message }}
- name: Create release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
with:
tag_name: ${{ steps.get-version.outputs.version }}
name: ${{ steps.commit.outputs.title }}
body: ${{ steps.commit.outputs.body }}
- name: Send release to Discord
run: node scripts/release/discord-release-message.js "${{ steps.get-version.outputs.version }}"
env:
DISCORD_RELEASES_ROLE_ID: ${{ secrets.DISCORD_RELEASES_ROLE_ID }}
DISCORD_RELEASES_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASES_WEBHOOK_URL }}
continue-on-error: true
font:
name: Trigger simple-icons-font release
needs: npm
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
repositories: 'simple-icons-font'
- name: Trigger simple-icons-font release
run: |
curl -X POST \
-H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \
-d '{"ref":"develop"}' \
https://api.github.com/repos/simple-icons/simple-icons-font/actions/workflows/auto-release.yml/dispatches
website:
name: Trigger simpleicons.org website update
needs: npm
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
repositories: 'simple-icons-website-rs'
- name: Trigger simpleicons.org website update
run: |
curl -X POST \
-H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \
-d '{"ref":"master"}' \
https://api.github.com/repos/simple-icons/simple-icons-website-rs/actions/workflows/auto-release.yml/dispatches

View File

@@ -0,0 +1,79 @@
name: Remove Outdated Labels
on:
pull_request_target:
types:
- closed
issues:
types:
- closed
permissions:
issues: write
pull-requests: write
jobs:
remove-merged-pr-labels:
name: Remove merged pull request labels
if: github.event.pull_request.merged
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- uses: mondeja/remove-labels-gh-action@v2
with:
token: ${{ steps.app-token.outputs.token }}
labels: |
assessing
awaiting reply
duplicate
in discussion
invalid
out of scope
pending
permission in review
permission required
won't add
remove-closed-pr-labels:
name: Remove closed pull request labels
if: |
github.event_name == 'pull_request_target' &&
(!github.event.pull_request.merged)
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- uses: mondeja/remove-labels-gh-action@v2
with:
token: ${{ steps.app-token.outputs.token }}
labels: |
in discussion
pending
assessing
permission in review
remove-closed-issue-labels:
name: Remove closed issue labels
if: github.event.issue.state == 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- uses: mondeja/remove-labels-gh-action@v2
with:
token: ${{ steps.app-token.outputs.token }}
labels: |
in discussion
pending
assessing
permission in review

View File

@@ -0,0 +1,50 @@
name: Renovate
on:
schedule:
- cron: '0 0 1 5,11 *'
workflow_dispatch:
jobs:
check-is-fork:
name: Check if running in a fork
runs-on: ubuntu-latest
permissions: {}
outputs:
is-fork: ${{ steps.check.outputs.is-fork }}
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/check-is-fork
id: check
with:
in-fork-message: 'Renovate workflow only can run in the main repository, skipping.'
renovate:
runs-on: ubuntu-latest
needs: check-is-fork
if: needs.check-is-fork.outputs.is-fork != 'true'
timeout-minutes: 15
permissions:
contents: write
issues: write
pull-requests: write
checks: write
statuses: write
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v5
- name: Setup node
uses: actions/setup-node@v6
with:
node-version-file: .node-version
- name: Renovate
uses: renovatebot/github-action@v43.0.19
with:
configurationFile: .github/renovate.json5
token: ${{ steps.app-token.outputs.token }}
env:
LOG_LEVEL: debug

View File

@@ -0,0 +1,124 @@
name: Verify Source
on:
pull_request:
push:
branches:
- master
- develop
jobs:
build:
name: Build package
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: npm
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
run: npm ci --no-audit --no-fund
- name: Build NodeJS package
run: npm run build
lint:
name: Lint
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: npm
cache-dependency-path: '**/package-lock.json'
- name: Is normal pull request
# check if is not a release pull request
id: pr
run: echo "is_normal=$(echo ${{ github.base_ref != 'master' && github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'meta') == false }})" >> $GITHUB_OUTPUT
- name: Detect changed files
uses: dorny/paths-filter@v3
id: changes
with:
list-files: shell
filters: |
docs:
- '*!(slugs).md'
- '.github/**.md'
icons:
- 'icons/*.svg'
slugs:
- 'slugs.md'
sdkts:
- 'sdk.d.ts'
token: ${{ secrets.GITHUB_TOKEN }}
- name: Don't edit slugs.md in pull requests
if: steps.pr.outputs.is_normal == 'true' && steps.changes.outputs.slugs == 'true'
run: |
echo -ne "Detected slugs.md file edition in PR.\n" 1>&2
echo -ne "Please revert it, we build the slugs.md" 1>&2
echo -ne " file automatically at releases.\n" 1>&2
exit 1
- name: Don't edit sdk.d.ts in pull requests
if: steps.pr.outputs.is_normal == 'true' && steps.changes.outputs.sdkts == 'true'
run: |
echo -ne "Detected sdk.d.ts file edition in PR.\n" 1>&2
echo -ne "Please revert it, we build the sdk.d.ts" 1>&2
echo -ne " file automatically at releases.\n" 1>&2
exit 1
- name: Install dependencies
run: npm ci --no-audit --no-fund
- name: Prepare icons
if: steps.pr.outputs.is_normal == 'true' && steps.changes.outputs.icons == 'true'
run: sed -i 's/\\"icons\/\*\.svg\\"/$npm_config_icons/' package.json
- name: Prepare docs (docs changed)
# only lint changed docs
if: steps.pr.outputs.is_normal == 'true' && steps.changes.outputs.docs == 'true'
run: sed -i "s/'\*\*\/\*\.md'/"'$npm_config_docs'"/" package.json
- name: Prepare docs (no docs changed)
# only lint README.md if no changes in docs
if: steps.pr.outputs.is_normal == 'true' && steps.changes.outputs.docs != 'true'
run: sed -i "s/'\*\*\/\*\.md'/README.md/" package.json
- name: Run linter
run: npm run lint --icons="$ICONS_FILES" --docs="$DOCS_FILES"
env:
ICONS_FILES: ${{ steps.changes.outputs.icons_files }}
DOCS_FILES: ${{ steps.changes.outputs.docs_files }}
# Authorise GitHub API requests for editorconfig-checker
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check documentation links
if: steps.changes.outputs.docs == 'true'
uses: lycheeverse/lychee-action@v2
with:
args: -- ${{ steps.changes.outputs.docs_files }}
fail: ${{ github.ref != 'refs/heads/develop' }}
failIfEmpty: false
jobSummary: true
format: markdown
token: ${{ secrets.GITHUB_TOKEN }}
test:
name: Test package
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: npm
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
run: npm ci --no-audit --no-fund
- name: Run tests
run: npm run test

View File

@@ -0,0 +1,60 @@
# Editor configurations
.vscode
.idea
# Files generated by build script
/index.js
/index-icons.js
/index.mjs
/index-icons.mjs
/index.d.ts
/sdk.js
# Ignore all files in the icons folder
icons/*
# Except SVG files
!icons/*.svg
### NodeJS ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
yarn.lock
# Dependency directories
node_modules/
# Generated files
*.tgz
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env sh
# ^^^^^^^^^^^^^^^ Husky doesn't require this shebang,
# but code editors need it to recognize the file as a script.
# Format and add the changes to the staging area
npm run format
git add --update
# POSIX shell (sh) run time parameters.
# -e: Exit immediately if a command exits with a non-zero status.
# -u: Treat unset variables as errors when substituting.
set -eu
# Track exit code in case that we want to add other lints in the future
EXITCODE=0
# If there are changed icons, lint them with SVGLint
changed_icons=$(git diff --cached --name-only --diff-filter=ACM 'icons/' | xargs)
[ -n "$changed_icons" ] && { npm run svglint:base -- $changed_icons || EXITCODE=$?; }
exit $EXITCODE

View File

@@ -0,0 +1,167 @@
{
"title": "Simple Icons",
"definitions": {
"brand": {
"$id": "#brand",
"description": "A single brand",
"type": "object",
"required": ["title", "hex", "source"],
"properties": {
"title": {
"$ref": "#/definitions/title"
},
"slug": {
"$ref": "#/definitions/slug"
},
"hex": {
"description": "The brand color",
"$ref": "#/definitions/hex"
},
"source": {
"description": "The website from which the icon was sourced",
"$ref": "#/definitions/sourceUrl"
},
"guidelines": {
"description": "The brand guidelines",
"$ref": "#/definitions/url"
},
"aliases": {
"description": "The aliases for the brand",
"type": "object",
"properties": {
"aka": {
"description": "The brand is also known as (e.g. full length name or abbreviation)",
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"dup": {
"description": "Different brands that use the exact same icon",
"type": "array",
"items": {"$ref": "#/definitions/duplicate"},
"uniqueItems": true
},
"loc": {
"description": "Localized names of the brand",
"$ref": "#/definitions/locale"
},
"old": {
"description": "Old names, for backwards compatibility",
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
}
},
"minProperties": 1,
"additionalProperties": false
},
"license": {
"description": "The license for the icon",
"oneOf": [
{
"type": "object",
"required": ["type"],
"properties": {
"type": {
"description": "An SPDX License Identifier",
"type": "string"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": ["type", "url"],
"properties": {
"type": {
"description": "Custom license type",
"const": "custom"
},
"url": {
"description": "The URL to the license text by the brand",
"$ref": "#/definitions/url"
}
},
"additionalProperties": false
}
]
}
},
"additionalProperties": false
},
"duplicate": {
"$id": "#duplicate",
"description": "A brand that uses the same icon",
"type": "object",
"required": ["title"],
"properties": {
"title": {
"$ref": "#/definitions/title"
},
"slug": {
"$ref": "#/definitions/slug"
},
"hex": {
"description": "The brand color, if different from the original",
"$ref": "#/definitions/hex"
},
"source": {
"description": "The website from which the duplicate's hex was sourced, if different from the original",
"$ref": "#/definitions/url"
},
"guidelines": {
"description": "The brand guidelines, if different from the original",
"$ref": "#/definitions/url"
},
"loc": {
"description": "Localized names of the brand",
"$ref": "#/definitions/locale"
}
},
"additionalProperties": false
},
"hex": {
"$id": "#hex",
"description": "A 6-character hexadecimal color value (without #)",
"type": "string",
"pattern": "^[0-9A-F]{6}$"
},
"locale": {
"$id": "#locale",
"description": "A localized brand name",
"type": "object",
"patternProperties": {
"^[a-z]{2}-[A-Z]{2}$": {
"type": "string",
"description": "The local name of the brand"
}
},
"minProperties": 1,
"additionalProperties": false
},
"slug": {
"$id": "#slug",
"description": "The brand name slug (used as filename in icons/)",
"type": "string",
"pattern": "^[a-z0-9]+(_[a-z0-9]+)?$"
},
"title": {
"$id": "#title",
"description": "The name of the brand",
"type": "string"
},
"sourceUrl": {
"$id": "#sourceUrl",
"description": "URL for icon source. If is a GitHub URL, is validated to contain a commit hash, to be an issue comment or to be a GitHub organization URL",
"$ref": "#/definitions/url"
},
"url": {
"$id": "#url",
"description": "HTTPS-only URL for a source",
"type": "string",
"pattern": "^https://[^\\s]+$"
}
},
"type": "array",
"items": {"$ref": "#/definitions/brand"}
}

View File

@@ -0,0 +1,6 @@
{
"default": true,
"MD013": false,
"MD033": false,
"MD041": false
}

View File

@@ -0,0 +1 @@
24

View File

@@ -0,0 +1,21 @@
# Ignore all files in root
/*
# Except the ones we want to publish
!data/
!icons/
!package.json
!DISCLAIMER.md
!README.md
!LICENSE.md
!VERSIONING.md
!index.js
!index-icons.js
!index.mjs
!index-icons.mjs
!index.d.ts
!types.d.ts
!sdk.mjs
!sdk.js
!sdk.d.ts
!.jsonschema.json
!jsconfig.json

View File

@@ -0,0 +1,3 @@
package-lock=true
save-exact=true
save-dev=true

View File

@@ -0,0 +1 @@
24

View File

@@ -0,0 +1,8 @@
# We prefer our own custom formatting for MarkDown files.
# See the following thread for the discussion:
# https://github.com/simple-icons/simple-icons-font/pull/73
LICENSE.md
slugs.md
# We use our own formatting for the data files.
data/simple-icons.json

View File

@@ -0,0 +1,8 @@
{
"plugins": ["prettier-plugin-packagejson"],
"useTabs": true,
"singleQuote": true,
"bracketSpacing": false,
"proseWrap": "never",
"embeddedLanguageFormatting": "off"
}

View File

@@ -0,0 +1,79 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at <community@simpleicons.org>. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of actions.
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at <https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at <https://www.contributor-covenant.org/faq>. Translations are available at <https://www.contributor-covenant.org/translations>.

View File

@@ -0,0 +1,554 @@
<!-- markdownlint-disable no-blanks-blockquote -->
# Contributing to Simple Icons
[![Good first issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Fsearch%2Fissues%3Fq%3Drepo%3Asimple-icons%2Fsimple-icons%2520label%3A%2522good%2520first%2520issue%2522%2520is%3Aopen%2520-linked%3Apr&query=%24.total_count&suffix=%20open&logo=github&label=good%20first%20issues&color=228f6c&labelColor=228f6c&logoColor=white&style=flat-square)](https://github.com/simple-icons/simple-icons/issues?q=is%3Aopen+label%3A%22good+first+issue%22+-linked%3Apr) [![Icon issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Fsearch%2Fissues%3Fq%3Drepo%3Asimple-icons%2Fsimple-icons%2520label%3A%2522update%2520icon%2Fdata%2522%2C%2522new%2520icon%2522%2520is%3Aopen%2520-linked%3Apr&query=%24.total_count&suffix=%20open&logo=svg&logoColor=333&label=icon%20issues&labelColor=FFB13B&color=FFB13B&style=flat-square)](https://github.com/simple-icons/simple-icons/issues?q=is%3Aissue+is%3Aopen+label%3A%22new+icon%22%2C%22update+icon%2Fdata%22) [![Code issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Fsearch%2Fissues%3Fq%3Drepo%3Asimple-icons%2Fsimple-icons%2520is%3Aissue%2520is%3Aopen%2520label%3Ameta%2Cpackage%2520-linked%3Apr&query=%24.total_count&suffix=%20open&logo=typescript&logoColor=white&label=code%20issues&labelColor=3178C6&color=3178C6&style=flat-square)](https://github.com/simple-icons/simple-icons/issues?q=is%3Aissue+is%3Aopen+label%3Adocs%2Cmeta%2Cpackage+-linked%3Apr) [![Documentation issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Fsearch%2Fissues%3Fq%3Drepo%3Asimple-icons%2Fsimple-icons%2520label%3Adocs%2520is%3Aopen%2520-linked%3Apr&query=%24.total_count&suffix=%20open&logo=markdown&label=docs%20issues&labelColor=343a40&color=343a40&logoColor=FFF&style=flat-square)](https://github.com/simple-icons/simple-icons/issues?q=is%3Aopen+is%3Aissue+label%3Adocs+-linked%3Apr)
> [!IMPORTANT]\
> We ask that all users read our [legal disclaimer](./DISCLAIMER.md) before contributing to Simple Icons.
Simple Icons welcomes contributions and corrections. Before contributing, please make sure you have read the guidelines below. If you decide to contribute anything, please follow the steps below. If you're new to _git_ and/or _GitHub_, we suggest you go through [the GitHub Guides](https://guides.github.com/introduction/flow/).
1. Fork this repository
1. (Optional) Clone the fork
- Using SSH
```shell
git clone --filter=tree:0 git@github.com:simple-icons/simple-icons.git
```
- Using HTTPS
```shell
git clone --filter=tree:0 https://github.com/simple-icons/simple-icons.git
```
- Using GitHub CLI
```shell
gh repo clone simple-icons/simple-icons -- --filter=tree:0
```
1. Create a new branch from the latest `develop`
1. Start hacking on the new branch
1. Commit and push to the new branch
1. Make a pull request
## Table of contents
- [Requesting an Icon](#requesting-an-icon)
- [Forbidden Brands](#forbidden-brands)
- [Assessing Popularity](#assessing-popularity)
- [Opening an Issue](#opening-an-issue)
- [Adding or Updating an Icon](#adding-or-updating-an-icon)
- [Requesting Permission](#requesting-permission)
- [Testing Package Locally](#testing-package-locally)
- [Using Docker](#using-docker)
## Requesting an Icon
We welcome icon requests. Before you submit a new issue please make sure the icon:
- Has not already been requested. If you find an existing issue or pull request for the brand you're looking for then please add a reaction or comment to show your support.
- Is of a _popular_ brand - see "[Assessing Popularity](#assessing-popularity)" below.
- Doesn't fall into one of the following categories:
- Illegal services (e.g. piracy, malware, threatening material, spam, etc.)
- Military-related brands and organizations
- Governmental agencies, programs, departments
- Allowed: International organizations and NGOs with supranational interests
- Allowed: Space agencies
- Allowed: Meteorological services
- Allowed: State owned media (unless engaged in propaganda)
- Religious institutions and organisations
- Brands that operate in service of a religion or faith
- Allowed: Brands where religion or faith are part of the corporate culture
- Symbols, including flags and banners
- Allowed: standards like FCC, CE, CCC, RoHS...
- Allowed: licenses like CC, Unlicense, MIT...
- Sport clubs
- Allowed: Sports organizations
- Yearly releases
- Universities or other educational institutions
- Any brands representing individuals rather than an organization, company, or product. This includes musicians, bands, and social media personalities.
### Forbidden Brands
Some companies and organizations are excessively protective with their brands, so please don't consider them:
- [Amazon / AWS](https://github.com/simple-icons/simple-icons/pull/13056)
- BP
- Disney, including, but not limited to:
- Marvel
- International Olympic Committee
- Mattel
- Microchip Technology Inc.
- [Microsoft](https://github.com/simple-icons/simple-icons/issues/11236), including, but not limited to:
- C#
- LinkedIn
- Playwright
- Visual Studio
- Oracle, including, but not limited to:
- Java
- [Yahoo!](https://github.com/simple-icons/simple-icons/pull/9861#issuecomment-1819664495)
- Do you know more? Please, [report them](https://github.com/simple-icons/simple-icons/issues/new?labels=docs&template=documentation.yml).
If you are in doubt, feel free to submit it and we'll have a look.
### Assessing Popularity
> [!NOTE]\
> Please make your way through the following metrics _in order_, only moving on to the next metric if the current metric is not applicable to the brand being assessed or if it would put the brand outside our scope.
To be considered popular enough to be within our scope, a brand must be in existence for at least one year and meet one of the following metrics of popularity, in order of preference:
1. Its website's Traffic.cv global rank is in the top 500k.
- As Traffic.cv updates its data only once every month, there will be a monitoring window for websites ranked between 450k & 550k until the next update, _unless_ the brand is within scope on any other metric below.
- For existing icons in our library, the threshold is dropped from 500k to 750k.
- A rank lower than 2m, without any other metric being provided, will result in the brand being declared outside our scope.
1. The website's Traffic.cv rank in any one country is either:
- In the top 100, or,
- In the top 10k, _with_ a global rank of 1m or better.
1. Its app's Traffic.cv country rank, in any one country, is in the top 1k for either Android or iOS.
- Android apps should be assessed by their usage rank, rather than their store rank.
- Category ranks will not be accepted.
1. In cases where a brand does not have its own website the Traffic.cv rank of its parent company will be accepted _if_ the brand is that company's primary product.
1. Its packages meet one of the following minimum requirements:
- [npm](https://www.npmjs.com): 100k weekly downloads,
- [jsDelivr](https://www.jsdelivr.com): 1m daily or 35m monthly requests,
- [crates.io](https://crates.io): 1,200,000 recent (100k weekly) downloads,
- [PyPi Stats](https://pypistats.org): 100k weekly downloads,
- [Homebrew Formulae](https://formulae.brew.sh): 5k installs in the last 30 days or 50k installs in the last year,
- [Arch User Repository](https://aur.archlinux.org): popularity of 7.00, or,
- [Docker](https://www.docker.com): 500k weekly downloads.
1. If the brand is a Fediverse project, it needs to have a minimum of 10,000 Monthly Active Users as listed on [FediDB](https://fedidb.org/software).
- In addition, for **servers** that have a unique recognizable logo, the User Count as listed on [FediDB](https://fedidb.org/network) should be 100,000 or higher.
1. The brand's popularity can be illustrated by other publicly available & verifiable statistic (e.g., downloads, usage).
- Stats should preferably also include data on one of our existing brands so a direct comparison can be made.
1. The brand's popularity can be illustrated through a worldwide Google Trends comparison, or similar.
- Must be with a similar brand that is already in our library and still within our scope, or that would qualify under any metric.
- Must be unambiguous (i.e., it's not a suitable metric for brands with generic words for names).
- Trending equal to or higher than the compared brand will be considered in scope.
- Trending lower than the compared brand but with an upward trajectory will require the consensus of the person providing the comparison and at least 2 project maintainers.
As a last resort, after exhausting all other options above, and where applicable, the primary repository for the brand's GitHub project will be considered provided it meets the following requirements (see note below):
- It is active,
- It has a minimum of 5k GitHub stars,
- It has an average of 1k stars for each year of its existence (e.g., a project started 10 years ago would require a minimum of 10k stars), and,
- Its [star history](https://star-history.com) is on a consistently upward trajectory.
If all else fails, though, feel free to make a good case for the popularity of the brand you're requesting on any other grounds, provided it can be backed up with verifiable data. Example: a car manufacturer's own website falls outside our scope but a major dealership dealing exclusively or primarily in that brand falls within our scope - in that case we'd accept the manufacturer as being popular. If you can provide a _particularly_ good metric that can be applied to other brands then it will be added to this list.
> [!NOTE]\
> All metrics & cut-offs above are subject to ongoing review and potential change. These current metrics were devised in order to no longer rely on GitHub stars as an indication of a project's popularity. But, for projects that only exist on GitHub, we do still need some way of assessing their popularity so, until we can come up with a better way to do so, we have kept stars for the time being but will not be putting as much weight on them as in the past. If you know of a better way of assessing the popularity of a GitHub project then please create an issue for discussion.
### Opening an Issue
When submitting a request for a new or updated icon include helpful information such as:
- **Issue Title:** The brand name. For example:
- New Icons: `Request: GitHub Icon`
- Icon Updates: `Update: GitHub Color` or `Update: GitHub Icon`
- **Issue Body:** Links to official sources for the brand's icon and colors (e.g. media kits, brand guidelines, SVG files, etc.)
If you have an affiliation to the brand you are requesting that allows you to speak on their behalf then please disclose that in your issue as it can help speed up our research process.
Don't submit multiple requests in one issue. If you want to request multiple icons, please create a separate issue for each one.
## Adding or Updating an Icon
> [!NOTE]\
> If you decide to add an icon without requesting it first, the requirements above still apply.
### 1. Identify Official Logos and Colors
Most of the icons and brand colors on SimpleIcons have been derived from official sources. Using official sources helps ensure that the icons and colors in SimpleIcons accurately match the brand they represent. Thankfully, this is usually a simple process as organizations often provide brand guides and high-quality versions of their logo for download.
Official high quality brand logos and brand colors can usually be found in the following locations:
1. About pages, Press pages, Media Kits, and Brand Guidelines.
1. Website headers
1. Favicons
1. Wikimedia (which should provide a source)
1. GitHub repositories
It may be the case that no official source exists, but an unofficial icon has gained widespread acceptance and popularity. In such cases the unofficial icon can be included, but the details will be judged on a case-by-case basis. The JavaScript icon is an example of this. Notice that an unofficial source will never supersede an official one, even if it is more popular. An unofficial icon will only be accepted if no official option exists.
#### Icon Guidelines
Working with an SVG version of the logo is best. In the absence of an SVG version, other vector filetypes may work as well (e.g. EPS, AI, PDF). In the absence of vector logos, a vector can be created from a high-quality rasterized image, however, this is much more labor-intensive.
If the icon includes a (registered) trademark icon we follow the guidelines below to decide whether to include the symbol or not:
- If brand guidelines explicitly require including the symbol, it must be included.
- If the brand itself includes the symbol with all uses of the logo, even at small sizes, it must be included.
- If the symbol is incorporated into the design of the logo (e.g. [Chupa Chups]), it must be included.
- If there is ambiguity about the conditions under which the symbol is required, it must be included if it is a _registered trademark symbol_ (®) but not if is a _trademark symbol_ (™).
- If brand guidelines say it _may_ be removed, usually when the icon is displayed at small sizes, it must not be included.
- If there is no explicit requirement that a symbol must be included, it must not be included.
[Chupa Chups]: https://github.com/simple-icons/simple-icons/blob/develop/icons/chupachups.svg
#### Color Guidelines
For color, the brand's primary color should be used. The official color of a brand is usually found in their brand guidelines, media kits, or some of the other locations mentioned above. If no official color can be identified, use the brand's primary web color or the most prominent color in the logo itself (please indicate why you choose the particular color in your pull request). If an icon's primary color is made up of a gradient, use [Eric Meyer's Color Blender tool] to pick the color of the calculated midpoint. Simple Icons stores brand colors in the standard 6 character hexadecimal format.
[Eric Meyer's Color Blender tool]: https://meyerweb.com/eric/tools/color-blend/#::1:hex
### 2. Extract the Icon from the Logo
There are many different tools for editing SVG files, some options include:
| Name | Description | Platform | Price |
| :-- | :-- | :-: | :-: |
| [Inkscape](https://inkscape.org/) | Vector Graphics Editor | Windows, Mac, Linux | Free |
| [SVGEdit](https://svgedit.netlify.app/editor/index.html) | Vector Graphics Editor | Web | Free |
| [Boxy SVG](https://boxy-svg.com/) | Vector Graphics Editor | Windows, Mac, Linux | $ / Free (Linux, Web) |
| [Affinity Designer](https://affinity.serif.com/designer/) | Vector Graphics Editor | Windows, Mac | $ |
| [Adobe Illustrator](https://www.adobe.com/products/illustrator.html) | Vector Graphics Editor | Windows, Mac | $ - $$$ |
Using your preferred tool you should:
1. Isolate the icon from any text or extraneous items.
1. Merge any overlapping paths.
1. Compound all paths into one.
1. Change the icon's viewbox/canvas/page size to 24x24.
1. Scale the icon to fit the viewbox, while preserving the icon's original proportions. This means the icon should be touching at least two sides of the viewbox.
1. Center the icon horizontally and vertically.
1. Remove all colors. The icon should be monochromatic.
1. Export the icon as an SVG.
Some icons can't be easily converted to a monochromatic version due to colour changes, shadows, or other effects. For such cases, the addition of gaps is the recommended approach, with a preferred width of 0.5px. In some situations, a different gap may be required, but that will be determined on a per-case basis.
If you have any problems or questions while creating the SVG, check out [the GitHub Discussions]. You may find an answer to your question there or you can ask your question if you did not find an answer.
[the GitHub Discussions]: https://github.com/simple-icons/simple-icons/discussions/categories/help-with-svgs
### 3. Optimize the Icon
All icons in Simple Icons have been optimized with the [SVGO tool]. This can be done in one of three ways:
- The [SVGO Command Line Tool](https://github.com/svg/svgo)
- Run the following command `npx svgo icons/file-to-optimize.svg`
- Check if there is a loss of quality in the output, if so increase the precision.
- The [SVGOMG Online Tool](https://jakearchibald.github.io/svgomg/)
- Click "Open SVG" and select an SVG file.
- Set the precision to about 3, depending on if there is a loss of quality.
- Leave the remaining settings untouched (or reset them with the button at the bottom of the settings).
- Click the download button.
- The [SVGO Command Line Tool](https://github.com/svg/svgo) in Docker
- If none of the options above work for you, it is possible to build a Docker image for compressing the images.
- Build: `docker build . -t simple-icons`
- Run: `docker run --rm -v ${PWD}/icons/file-to-optimize.svg:/image.svg simple-icons`
After optimizing the icon, double-check it against your original version to ensure no visual imperfections have crept in. Also, make sure that the dimensions of the path have not been changed so that the icon no longer fits exactly within the canvas. We currently check the dimensions up to a precision of 3 decimal points.
[SVGO tool]: https://github.com/svg/svgo
### 4. Annotate the Icon
Each icon in Simple Icons has been annotated with a number of attributes and elements to increase accessibility. These include:
- An svg element with the following attributes, ordered respectively:
- An img role attribute.
- `role="img"`
- A 24x24 viewbox.
- `viewBox="0 0 24 24"`
- The svg namespace.
- `xmlns="http://www.w3.org/2000/svg"`
- A title element containing the brand name.
- `<title>Adobe Photoshop</title>`
Here is _part of_ the svg for the Adobe Photoshop icon as an example:
```svg
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Adobe Photoshop</title>...</svg>
```
### 5. Check the Icon
The final icon should:
- Be properly annotated [as discussed above](#4-annotate-the-icon).
- Be monochromatic.
- Remove all fill colors so that icon defaults to black.
- Be scaled to fit the viewbox, while preserving the icon's original proportions.
- This means the icon should be touching at least two sides of the viewbox.
- Be vertically and horizontally centered.
- Be minified to a single line with no formatting.
- Contain only a single `path` element.
- Not contain extraneous elements.
- This includes: `circle`, `ellipse`, `rect`, `polygon`, `line`, `g`, etc.
- Not contain extraneous attributes.
- This includes: `width`, `height`, `fill`, `stroke`, `clip`, `font`, etc.
Here is the svg for the Adobe Photoshop icon as an example:
```svg
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Adobe Photoshop</title><path d="M0 .3v23.4h24V.3H0zm1 1h22v21.4H1V1.3zm4.8 4.48c0-.067.14-.116.224-.116.644-.033 1.588-.05 2.578-.05 2.772 0 3.85 1.52 3.85 3.466 0 2.54-1.842 3.63-4.102 3.63-.38 0-.51-.017-.775-.017v3.842c0 .083-.033.116-.115.116H5.916c-.083 0-.115-.03-.115-.113V5.78zm1.775 5.312c.23.016.412.016.81.016 1.17 0 2.27-.412 2.27-1.996 0-1.27-.786-1.914-2.122-1.914-.396 0-.775.016-.957.033v3.864zm8.607-1.188c-.792 0-1.056.396-1.056.726 0 .363.18.61 1.237 1.155 1.568.76 2.062 1.485 2.062 2.557 0 1.6-1.22 2.46-2.87 2.46-.876 0-1.62-.183-2.05-.43-.065-.033-.08-.082-.08-.165V14.74c0-.1.048-.133.114-.084.624.413 1.352.594 2.012.594.792 0 1.122-.33 1.122-.776 0-.363-.23-.677-1.237-1.205-1.42-.68-2.014-1.37-2.014-2.527 0-1.287 1.006-2.36 2.755-2.36.86 0 1.464.132 1.794.28.082.05.1.132.1.198v1.37c0 .083-.05.133-.15.1-.444-.264-1.1-.43-1.743-.43z"/></svg>
```
### 6. Name the Icon
The filename of the SVG should correspond to the `<title>` used in the markup file mentioned above, and it should follow the rules below. If you're in doubt, you can always run `npm run get-filename -- "Brand name"` to get the correct filename.
1. Use **lowercase letters** without **whitespace**, for example:
```yml
title: Adobe Photoshop
filename: adobephotoshop.svg
```
1. Only use **latin** letters, for example:
```yml
title: Citroën
filename: citroen.svg
```
1. Replace the following symbols with their alias:
| Symbol | Alias |
| :----: | ----- |
| + | plus |
| . | dot |
| & | and |
for example:
```yml
title: .Net
filename: dotnet.svg
```
1. On rare occasions the resulting name will clash with the name of an existing SVG file in our collection. To resolve such conflicts append `_[MODIFIER]` to the name, where `[MODIFIER]` is a short descriptor of the brand or the service they provide and follows the same rules of construction as above.
for example:
```yml
title: Hive
filename: hive_blockchain.svg
```
### 7. Update the JSON Data for SimpleIcons.org
Icon metadata should be added to the `data/simple-icons.json` file. Each icon in the array has three required values:
- The `title` of the new SVG.
- A `hex` color value that matches the brand's primary color. All uppercase and without the `#` symbol.
- The `source` URL of the logo being used. There are [more details below](#source-guidelines).
There are also [optional values](#optional-data) that may be provided for each icon, which are listed below.
Here is the object of a fictional brand as an example:
```json
{
"title": "A Fictional Brand",
"hex": "123456",
"source": "https://www.a-fictional-brand.org/logo"
}
```
You can use `npm run add-icon-data` to add metadata via a CLI prompt.
Make sure the icon is added in alphabetical order. If you're in doubt, you can always run `npm run ourlint` - this will tell you if any of the JSON data is in the wrong order.
#### Optional Data
Additionally, each icon in the `data/simple-icons.json` file may be given any of the following optional values:
- The `slug` must be used to specify the icon's file name in cases where a modifier has been added to it in order to resolve a clash with an existing icon's name.
- The `guidelines` may be used to specify the URL of the brand's guidelines/press kit/etc. This is useful if the SVG file was sourced from a different place, still if the SVG file was sourced from the guidelines, the URL should be duplicated here.
- The `license` may be used to specify the license under which the icon is available. This is an object with a `type` and `url`. The `type` should be a [SPDX License ID](https://spdx.org/licenses/) or `"custom"`, the `url` is optional unless the `type` is `"custom"`.
Here is the object of the fictional brand from before, but with all optional values, as an example:
```json
{
"title": "A Fictional Brand",
"slug": "afictionalbrand_modifier",
"hex": "123456",
"source": "https://www.a-fictional-brand.org/logo",
"guidelines": "https://www.a-fictional-brand.org/brand-guidelines",
"license": {
"type": "CC0-1.0",
"url": "https://www.a-fictional-brand.org/logo/license"
}
}
```
> Non secured HTTP URLs are forbidden. If a brand's website only supports HTTP, you must still declare the URL using the `https://` protocol.
#### Source Guidelines
We use the source URL as a reference for the current SVG in our repository and as a jumping-off point to find updates if the logo changes. If you used one of the sources listed below, make sure to follow these guidelines. If you're unsure about the source URL you can open a Pull Request and ask for help from others.
If the SVG is sourced from:
- **Branding page**: For an SVG from a branding page the source URL should link to the branding page and not the image, PDF, or archive (such as `.zip`) file.
- **Company website**: If the SVG is found on the company website (but there is no branding page) the source URL should link to a common page, such as the home page or about page, that includes the source image and not the image file itself.
- **GitHub**: For an SVG from a GitHub (GitLab, BitBucket, etc.) repository the source URL should link to the file that was used as source material. If the color does not come from the file, its origin should be stated in the Pull Request description.
The commit hash should always be part of the URL. On GitHub, you can get the correct URL by pressing <kbd>y</kbd> on the GitHub page you want to link to. You can get help at the [getting permanent links to files page](https://help.github.com/en/github/managing-files-in-a-repository/getting-permanent-links-to-files).
- **Wikipedia**: For an SVG from Wikipedia/Wikimedia the source URL should link to the logo file's page on the relevant site, and not the brand's Wikipedia pages. For example, [this is the link for AmericanExpress](https://commons.wikimedia.org/wiki/File:American_Express_logo.svg).
In general, make sure the URL does not contain any tracking identifiers.
#### Aliases
Lastly, we aim to provide aliases of three types for various reasons. Each type of alias and its purpose can be found below. If you're unsure, you can mention an alias you're considering in your Pull Request so it can be discussed.
##### Also Known As
We collect "also known as" names to make it easier to find brands that are known by different names or by their abbreviation/full name. This does not include localized names, which are recorded separately. To add an "also known as" name you add the following to the icon data:
```json
{
"title": "the original title",
"aliases": {
"aka": [
"tot",
"thetitle"
]
}
}
```
Where the string is **different** from the original title as well as all other strings in the list.
##### Duplicates
We collect the names of duplicates, brands that use the same icon but have a different name, to prevent duplicating an SVG while at the same time making the SVG available under the name of the duplicate. To add a duplicate you add the following to the icon data:
```json5
{
"title": "the original title",
"hex": "123456",
"aliases": {
"dup": [
{
"title": "the duplicate's title",
"hex": "654321", // Only if different from original's color
"guidelines": "..." // Only if different from original's guidelines
}
]
}
}
```
Where the nested `title` is the name of the duplicate brand. The other fields, `hex` and `guidelines`, are only provided if they differ from the original.
##### Localization
We collect localized names to make it possible to find the brand by it's local name, as well as to provide SVGs with localized titles. To add a localized name you add the following to the icon data:
```json
{
"title": "the original title",
"aliases": {
"loc": {
"en-US": "A different title"
}
}
}
```
Where the `locale` is an [IETF language tag] and each localized title is **different** from the original one.
[IETF language tag]: https://en.wikipedia.org/wiki/IETF_language_tag
##### Old Names
We collect old names to make it possible to find the brand by it's old name. To add an old name you add the following to the icon data:
```json
{
"title": "the title",
"aliases": {
"old": [
"the old title"
]
}
}
```
Where each old title is **different** from the current title as well as all other strings in the list.
Old names may be retained indefinitely, but can be removed at any time, ideally when they no longer serve a meaningful purpose.
### 8. Create a Pull Request
Once you've completed the previous steps, create a pull request to merge your edits into the _develop_ branch. You can run `npm run lint` to check if there are any issues you still need to address.
If you have an affiliation to the brand you contributing that allows you to speak on their behalf then please disclose that in your pull request as it can help speed up our research and review processes.
Don't submit multiple icon additions or updates in one pull request. If you want to add or update multiple icons, please create a separate pull request for each one.
### Requesting Permission
If a brand's terms and conditions state that we need permission to include their brand, we will tag related issues and PRs with `permission required`. Anybody creating a PR for one of these icons is responsible (even if we discover permission requirements during PR review) for obtaining said permission.
If a contributor has been asked to obtain permission, they have 2 weeks to confirm they've reached out to the brand. If they do not confirm they have done this during that time, the PR will be closed.
If the contributor _has_ confirmed they've reached out to the brand, we will wait a maximum of 2 months for a response. If the response is an outright 'no' or is ambiguous, both the PR and issue will be closed, and the issue will marked as `will not add`. If the brand does not respond, we may close the PR or mark it as a draft, depending on whether the contributor is still actively pursuing a response. If the brand approves usage, the contributor should add the email response to the comments of the PR, and we will continue to merge the icon into the collection.
#### Example Email
```text
Dear [Brand Name],
I am contributing to the Simple Icons project on GitHub (https://github.com/simple-icons/simple-icons) and would like to add your brand's icon.
One of the maintainers has requested - as per your Terms and Conditions - that I reach out to formally obtain permission prior to accepting it into their collection.
Could you please let me know at your earliest convenience whether or not we are okay to include your brand's icon in the project?
Kind regards,
[Your name]
```
## Testing Package Locally
- Make sure you have [Node.js](https://nodejs.org/en/download/) installed. At least version `>=18.18.0` is required.
- Install the dependencies using `npm install`.
- Build and test the package using `npm test`.
- Run the project linting process using `npm run lint`.
## Using Dev Container
We have a pre-configured Node.js runtime for [Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers). You can also use the [GitHub Codespaces](https://github.com/features/codespaces) to code from any device.
## Using Docker
You can build a Docker image for this project which can be used as a development environment and allows you to run SVGO safely. First, build the Docker image for simple-icons (if you haven't yet):
```shell
docker build . -t simple-icons
```
Then, start a Docker container for simple-icons and attach to it:
```shell
docker run -it --rm --entrypoint "/bin/ash" simple-icons
```
## Developing Third-Party Extensions
An SDK is included in the `simple-icons/sdk` entrypoint of the npm package to make it easier the development of third party extensions with JavaScript and TypeScript.
```typescript
import {getIconsData, type IconData} from 'simple-icons/sdk';
const iconsData: Promise<IconData[]> = getIconsData();
```
```javascript
import {getIconsData} from 'simple-icons/sdk';
/* @typedef {import("./simple-icons/sdk").IconData} IconData */
/* @type {Promise<IconData[]>} */
const iconsData = getIconsData();
```

View File

@@ -0,0 +1,56 @@
# Disclaimer
Simple Icons asks that its users read this disclaimer fully before including an icon in their project.
## Table of Contents
- [Licenses, Copyrights & Trademarks](#licenses-copyrights--trademarks)
- [Brand Guidelines](#brand-guidelines)
- [Update of Brands](#update-of-brands)
- [Removal of Brands](#removal-of-brands)
## Licenses, Copyrights & Trademarks
> [!IMPORTANT]\
> The addition of licenses to Simple Icons is an ongoing project. Hence, the absence of licence data for a particular icon does not imply that the icon is not released under a license.
<!-- makdownlint-disable-next-line no-blanks-blockquote -->
> [!NOTE]\
> Simple Icons is released under CC0 - though that doesn't mean to imply that all icons within the project are also CC0. Please see individual licenses where available.
Simple Icons provides data on the license under which icons are available. We ask users to carefully consider this when using an icon. As licenses are subject to change we also ask our users to regularly check if the license of the icons they use have been changed.
The license under which an icon is available can be found in the icon's entry in the JSON data file in the NPM and Packagist packages, or through the license link on the icon's card on [the Simple Icons website].
As we rely largely on our community to help us keep Simple Icons up-to-date, the license data for an icon may be outdated. If you find that the license data provided is outdated, please [submit an issue][icon-outdated-issues] to [the Simple Icons GitHub repository].
If an icon includes a registered trademark (`®`) or trademark symbol (`™`) the recommendations outlined in [the Simple Icons Contributing Guidelines] are followed to decide whether to include the symbol or not.
Simple Icons cannot be held responsible for any legal activity raised by a brand, or users of the package. We ask that our users seek the correct permissions to use the icons relevant to their project.
## Brand Guidelines
> [!NOTE]\
> The addition of guidelines to Simple Icons is an ongoing project. In the meantime, users of Simple Icons are instead encouraged to check the `source` URL as, in some cases, the icon will have been sourced from official guidelines. The lack of a `guidelines` entry for a particular brand does not imply that the brand has no guidelines.
Simple Icons provides a link to a brand's _branding guidelines_ (or similar) if the brand provides one. We ask our users read these guidelines and ensure their usage of the brand's icon is in accordance with them. As guidelines are subject to change we also ask our users to regularly check if the brand guidelines of the icons they use have been updated.
The brand guidelines can be found in the icon's entry in the JSON data file in the NPM and Packagist packages, or through the "Brand Guidelines" link on the icon's card on [the Simple Icons website].
As we rely largely on our community to help us keep Simple Icons up-to-date, the guidelines link for a brand may be outdated. If you find that the guidelines are outdated, please [submit an issue][icon-outdated-issues] to [the Simple Icons GitHub repository].
## Update of Brands
Should a brand wish for their icon or data to be updated, please [submit an issue][icon-outdated-issues] to [the Simple Icons GitHub repository]. In the issue, please explain your affiliation with the company and reasons for the update. We generally release updates once a week, but can make exceptions for immediate updates of brands.
## Removal of Brands
Should a brand wish for their icon to be removed from the package, contact `removals at simpleicons dot org` explaining your affiliation with the company, and reasons for removal. Alternatively, it is also possible to [submit an issue][removal-issues] on [the Simple Icons GitHub repository] with the same information. We generally remove icons that no longer [meet our criteria] twice a year in our major releases - but can occasionally make exceptions for immediate removal of brands.
[meet our criteria]: https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#requesting-an-icon
[icon-outdated-issues]: https://github.com/simple-icons/simple-icons/issues/new?assignees=&labels=update+icon%2Fdata&template=icon_update.yml&title=Update%3A+
[removal-issues]: https://github.com/simple-icons/simple-icons/issues/new?assignees=&labels=breaking+change&template=icon_removal.yml&title=Remove%3A+
[the simple icons contributing guidelines]: https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#icon-guidelines
[the simple icons github repository]: https://github.com/simple-icons/simple-icons
[the simple icons website]: https://simpleicons.org/

View File

@@ -0,0 +1,10 @@
FROM node:24-alpine AS base
RUN apk add --no-cache git
WORKDIR /simple-icons
FROM base AS final
WORKDIR /simple-icons
COPY . .
RUN npm ci --no-audit --no-fund
ENTRYPOINT ["npx", "svgo", "/image.svg"]

View File

@@ -0,0 +1,30 @@
# CC0 1.0 Universal
## Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an “owner”) of an original work of authorship and/or a database (each, a “Work”).
Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works (“Commons”) that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the “Affirmer”), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights (“Copyright and Related Rights”). Copyright and Related Rights include, but are not limited to, the following:
1. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
2. moral rights retained by the original author(s) and/or performer(s);
3. publicity and privacy rights pertaining to a persons image or likeness depicted in a Work;
4. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(i), below;
5. rights protecting the extraction, dissemination, use and reuse of data in a Work;
6. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
7. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmers Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmers heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmers express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmers express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmers Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the “License”). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmers express Statement of Purpose.
4. Limitations and Disclaimers.
1. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
2. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
3. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any persons Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
4. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
For more information, please see <https://creativecommons.org/publicdomain/zero/1.0>.

View File

@@ -0,0 +1,225 @@
<p align="center">
<img src="https://cdn.simpleicons.org/simpleicons/000/fff" alt="Simple Icons" width=70>
<h3 align="center">Simple Icons</h3>
<p align="center">
Over 3300 SVG icons for popular brands. See them all on one page at <a href="https://simpleicons.org">SimpleIcons.org</a>. Contributions, corrections & requests can be made on GitHub.</p>
</p>
<p align="center">
<a href="https://simpleicons.org"><img src="https://img.shields.io/badge/dynamic/json?color=informational&label=icons&prefix=%20&logo=simpleicons&query=%24.length&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsimple-icons%2Fsimple-icons%2Fdevelop%2Fdata%2Fsimple-icons.json" alt="Number of icons currently in the library"/></a>
<a href="https://www.npmjs.com/package/simple-icons"><img src="https://img.shields.io/npm/v/simple-icons.svg?logo=npm" alt="NPM version"/></a>
<a href="https://packagist.org/packages/simple-icons/simple-icons"><img src="https://img.shields.io/packagist/v/simple-icons/simple-icons?logo=packagist&logoColor=white" alt="Build status"/></a>
<br/>
<a href="https://github.com/simple-icons/simple-icons/actions?query=workflow%3AVerify+branch%3Adevelop"><img src="https://img.shields.io/github/actions/workflow/status/simple-icons/simple-icons/verify.yml?branch=develop&logo=github&label=tests" alt="Build status"/></a>
<a href="https://discord.gg/vUXFa7t5xJ"><img src="https://img.shields.io/discord/1142044630909726760?logo=discord&logoColor=white&label=discord" alt="Number of users active in our Discord server"/></a>
<a href="https://opencollective.com/simple-icons"><img src="https://img.shields.io/opencollective/all/simple-icons?logo=opencollective" alt="Backers and sponsors on Open Collective"/></a>
</p>
## Usage
> [!IMPORTANT]\
> We ask that all users read our [legal disclaimer](https://github.com/simple-icons/simple-icons/blob/develop/DISCLAIMER.md) before using icons from Simple Icons.
### General Usage
Icons can be downloaded as SVGs directly from [simpleicons.org](https://simpleicons.org) - simply click the download button of the icon you want, and the download will start automatically.
### CDN Usage
Icons can be served from a CDN such as [jsDelivr](https://www.jsdelivr.com/package/npm/simple-icons) or [unpkg](https://unpkg.com/browse/simple-icons/). Simply use the `simple-icons` npm package and specify a version in the URL like the following:
```html
<img height="32" width="32" src="https://cdn.jsdelivr.net/npm/simple-icons@v15/icons/[ICON SLUG].svg" />
<img height="32" width="32" src="https://unpkg.com/simple-icons@v15/icons/[ICON SLUG].svg" />
```
Where `[ICON SLUG]` is replaced by the [slug] of the icon you want to use, for example:
```html
<img height="32" width="32" src="https://cdn.jsdelivr.net/npm/simple-icons@v15/icons/simpleicons.svg" />
<img height="32" width="32" src="https://unpkg.com/simple-icons@v15/icons/simpleicons.svg" />
```
These examples use the latest major version. This means you won't receive any updates following the next major release. You can use `@latest` instead to receive updates indefinitely. However, this will result in a `404` error if the icon is removed.
#### CDN with colors
We also provide a CDN service which allows you to use colors.
```html
<img height="32" width="32" src="https://cdn.simpleicons.org/[ICON SLUG]" />
<img height="32" width="32" src="https://cdn.simpleicons.org/[ICON SLUG]/[COLOR]" />
<img height="32" width="32" src="https://cdn.simpleicons.org/[ICON SLUG]/[COLOR]/[DARK_MODE_COLOR]" />
```
Where `[COLOR]` is optional, and can be replaced by the [hex colors](https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color) or [CSS keywords](https://www.w3.org/wiki/CSS/Properties/color/keywords) of the icon you want to use. The color is defaulted to the HEX color of the icon shown in [simpleicons.org website](https://simpleicons.org). `[DARK_MODE_COLOR]` is used for dark mode. The [CSS prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) will be used when a value is specified. For example:
```html
<img height="32" width="32" src="https://cdn.simpleicons.org/simpleicons" />
<img height="32" width="32" src="https://cdn.simpleicons.org/simpleicons/gray" />
<img height="32" width="32" src="https://cdn.simpleicons.org/simpleicons/hotpink" />
<img height="32" width="32" src="https://cdn.simpleicons.org/simpleicons/0cf" />
<img height="32" width="32" src="https://cdn.simpleicons.org/simpleicons/0cf9" />
<img height="32" width="32" src="https://cdn.simpleicons.org/simpleicons/00ccff" />
<img height="32" width="32" src="https://cdn.simpleicons.org/simpleicons/00ccff99" />
<img height="32" width="32" src="https://cdn.simpleicons.org/simpleicons/orange/pink" />
<img height="32" width="32" src="https://cdn.simpleicons.org/simpleicons/_/eee" />
<img height="32" width="32" src="https://cdn.simpleicons.org/simpleicons/eee/_" />
```
You can use a `viewbox=auto` parameter to get a auto-sized viewbox. This is useful if you want all icons rendered with consistent size:
```html
<img height="20" src="https://cdn.simpleicons.org/github?viewbox=auto" />
<img height="20" src="https://cdn.simpleicons.org/simpleicons?viewbox=auto" />
<img height="20" src="https://cdn.simpleicons.org/awesomelists?viewbox=auto" />
```
### Node Usage <img src="https://cdn.simpleicons.org/nodedotjs/000/fff" alt="Node" align=left width=24>
The icons are also available through our npm package. To install, simply run:
```shell
npm install simple-icons
```
All icons are imported from a single file, where `[ICON SLUG]` is replaced by a capitalized [slug]. We highly recommend using a bundler that can tree shake such as [webpack](https://webpack.js.org/) to remove the unused icon code:
```javascript
// Import a specific icon by its slug as:
// import { si[ICON SLUG] } from 'simple-icons'
// For example:
// use import/esm to allow tree shaking
import {siSimpleicons} from 'simple-icons';
// or with require/cjs
const {siSimpleicons} = require('simple-icons');
```
It will return an icon object:
```javascript
console.log(siSimpleicons);
/*
{
title: 'Simple Icons',
slug: 'simpleicons',
hex: '111111',
source: 'https://simpleicons.org/',
svg: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">...</svg>',
path: 'M12 12v-1.5c-2.484 ...',
guidelines: 'https://simpleicons.org/styleguide',
license: {
type: '...',
url: 'https://example.com/'
}
}
NOTE: the `guidelines` entry will be `undefined` if we do not yet have guidelines for the icon.
NOTE: the `license` entry will be `undefined` if we do not yet have license data for the icon.
*/
```
If you need to iterate over all icons, use:
```javascript
import * as icons from 'simple-icons';
```
#### TypeScript Usage <img src="https://cdn.simpleicons.org/typescript/000/fff" alt="Typescript" align=left width=19 height=19>
Type definitions are bundled with the package.
```typescript
import type {SimpleIcon} from 'simple-icons';
```
### PHP Usage <img src="https://cdn.simpleicons.org/php/000/fff" alt="Php" align=left width=24 height=24>
The icons are also available through our Packagist package. To install, simply run:
```shell
composer require simple-icons/simple-icons
```
The package can then be used as follows, where `[ICON SLUG]` is replaced by a [slug]:
```php
<?php
// Import a specific icon by its slug as:
echo file_get_contents('path/to/package/icons/[ICON SLUG].svg');
// For example:
echo file_get_contents('path/to/package/icons/simpleicons.svg');
// <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">...</svg>
?>
```
### Font Usage
See [simple-icons-font](https://github.com/simple-icons/simple-icons-font) to learn how to use our font distribution.
## Third-Party Extensions
The below are known extensions to third-party tools.
| Extension | Author |
| :-- | :-- |
| [Blender add-on](https://github.com/mondeja/simple-icons-blender) <img src="https://cdn.simpleicons.org/blender/000/fff" alt="Blender" align=left width=24 height=24> | [@mondeja](https://github.com/mondeja) |
| [Boxy SVG library](https://boxy-svg.com/ideas/298/simple-icons-library-provider) <img src="https://cdn.simpleicons.org/boxysvg/000/fff" alt="Boxy SVG" align=left width=24 height=24> | [@Jarek](https://boxy-svg.com/profiles/0000000000/jarek) |
| [Drawio library](https://github.com/mondeja/simple-icons-drawio) <img src="https://cdn.simpleicons.org/diagramsdotnet/000/fff" alt="Drawio" align=left width=24 height=24> | [@mondeja](https://github.com/mondeja) |
| [Figma plugin](https://www.figma.com/community/plugin/1149614463603005908) <img src="https://cdn.simpleicons.org/figma/000/fff" alt="Figma" align=left width=24 height=24> | [@LitoMore](https://github.com/LitoMore) |
| [Jekyll plugin](https://github.com/pirafrank/jekyll-simple-icons) <img src="https://cdn.simpleicons.org/jekyll/000/fff" alt="Jekyll" align=left width=24 height=24> | [@pirafrank](https://github.com/pirafrank) |
| [Kando icon theme](https://kando.menu/icon-themes#built-in-icon-themes) <img src="https://cdn.simpleicons.org/kando/000/fff" alt="Kando" align=left width=24 height=24> | [@Schneegans](https://github.com/Schneegans) |
| [Miro app](https://miro.com/marketplace/brand-icons/) <img src="https://cdn.simpleicons.org/miro/000/fff" alt="Miro" align=left width=24 height=24> | [@LitoMore](https://github.com/LitoMore) |
| [Raycast extension](https://www.raycast.com/litomore/simple-icons) <img src="https://cdn.simpleicons.org/raycast/000/fff" alt="Raycast" align=left width=24 height=24> | [@LitoMore](https://github.com/LitoMore) |
| [Stream Deck icon pack](https://github.com/mackenly/simple-icons-stream-deck) <img src="https://cdn.simpleicons.org/elgato/000/fff" alt="Stream Deck" align=left width=24 height=24> | [@mackenly](https://github.com/mackenly) |
| [Typst package](https://typst.app/universe/package/sicons) <img src="https://cdn.simpleicons.org/typst/000/fff" alt="Typst" align=left width=24 height=24> | [@cscnk52](https://github.com/cscnk52) |
| [Webflow app](https://webflow.com/apps/detail/simple-icons) <img src="https://cdn.simpleicons.org/webflow/000/fff" alt="Webflow" align=left width=24 height=24> | [@diegoliv](https://github.com/diegoliv) |
Maintain an extension? [Submit a PR][open-pr] to include it in the list above.
## Third-Party Libraries
The below are known third-party libraries for use in your own projects. We only keep items in the list that are at least up to date with our previous major version.
| Library | Author | License | Simple Icons |
| :-- | :-- | :-: | :-: |
| [Angular package](https://github.com/gridatek/semantic-icons/tree/main/libs/simple-icons) <img src="https://cdn.simpleicons.org/angular/000/fff" alt="Angular" align=left width=24 height=24> | [@gridatek](https://github.com/gridatek) | ![License](https://img.shields.io/github/license/gridatek/semantic-icons?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fgridatek%2Fsemantic-icons%2Fmain%2Fpackage.json&query=%24..devDependencies%5B'simple-icons'%5D&label=) |
| [Astro package](https://github.com/dzeiocom/simple-icons-astro) <img src="https://cdn.simpleicons.org/astro/000/fff" alt="Astro" align=left width=24 height=24> | [@Aviortheking](https://github.com/aviortheking) | ![License](https://img.shields.io/github/license/dzeiocom/simple-icons-astro?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fdzeiocom%2Fsimple-icons-astro%2Fmaster%2Fpackage.json&query=%24.version&label=) |
| [Blazor Nuget package](https://github.com/TimeWarpEngineering/timewarp-simple-icons) <img src="https://cdn.simpleicons.org/blazor/000/fff" alt="Blazor" align=left width=24 height=24> | [@TimeWarpEngineering](https://github.com/TimeWarpEngineering) | ![License](https://img.shields.io/github/license/TimeWarpEngineering/timewarp-simple-icons?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Fraw.githubusercontent.com%2FTimeWarpEngineering%2Ftimewarp-simple-icons%2Fmain%2Fsource%2Ftimewarp-simple-icons%2Ftimewarp-simple-icons.csproj&query=%2FProject%2FPropertyGroup%2FVersion&label=) |
| [Flutter package](https://github.com/jlnrrg/simple_icons) <img src="https://cdn.simpleicons.org/flutter/000/fff" alt="Flutter" align=left width=24 height=24> | [@jlnrrg](https://github.com/jlnrrg) | ![License](https://img.shields.io/github/license/jlnrrg/simple_icons?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fjlnrrg%2Fsimple_icons%2Fmaster%2Fpubspec.yaml&query=%24.version&logoColor=white&label=) |
| [Framer component](https://github.com/LitoMore/simple-icons-framer) <img src="https://cdn.simpleicons.org/framer/000/fff" alt="Framer" align=left width=24 height=24> | [@LitoMore](https://github.com/LitoMore) | ![License](https://img.shields.io/github/license/LitoMore/simple-icons-framer?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fsimple-icons%2Fsimple-icons%2Fmaster%2Fpackage.json&query=%24.version&label=) |
| [Hugo module](https://github.com/foo-dogsquared/hugo-mod-simple-icons) <img src="https://cdn.simpleicons.org/hugo/000/fff" alt="Hugo" align=left width=24 height=24> | [@foo-dogsquared](https://github.com/foo-dogsquared) | ![License](https://img.shields.io/github/license/foo-dogsquared/hugo-mod-simple-icons?label=) | ![Simple Icons version](https://img.shields.io/github/v/tag/foo-dogsquared/hugo-mod-simple-icons?label=) |
| [Java library](https://github.com/silentsoft/simpleicons4j) <img src="https://cdn.simpleicons.org/openjdk/000/fff" alt="Java" align=left width=24 height=24> | [@silentsoft](https://github.com/silentsoft) | ![License](https://img.shields.io/github/license/silentsoft/simpleicons4j?label=) | ![Simple Icons version](https://img.shields.io/maven-central/v/org.silentsoft/simpleicons4j?label=) |
| [Kirby plugin](https://github.com/runxel/kirby3-simpleicons) <img src="https://cdn.simpleicons.org/kirby/000/fff" alt="Kirby" align=left width=24 height=24> | [@runxel](https://github.com/runxel) | ![License](https://img.shields.io/github/license/runxel/kirby3-simpleicons?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Frunxel%2Fkirby3-simpleicons%2Fmaster%2Fcomposer.json&query=%24..%5B'simple-icons%2Fsimple-icons'%5D&label=) |
| [LaTeX package](https://github.com/ineshbose/simple-icons-latex) <img src="https://cdn.simpleicons.org/latex/000/fff" alt="LaTeX" align=left width=24 height=24> | [@ineshbose](https://github.com/ineshbose) | ![License](https://img.shields.io/github/license/ineshbose/simple-icons-latex?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fineshbose%2Fsimple-icons-latex%2Fdevelop%2Fpackage.json&query=%24..%5B'simple-icons-font'%5D&label=) |
| [Laravel package](https://github.com/ublabs/blade-simple-icons) <img src="https://cdn.simpleicons.org/laravel/000/fff" alt="Laravel" align=left width=24 height=24> | [@adrian-ub](https://github.com/adrian-ub) | ![License](https://img.shields.io/github/license/ublabs/blade-simple-icons?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fsimple-icons%2Fsimple-icons%2Fmaster%2Fpackage.json&query=%24.version&label=) |
| [Python wheel](https://github.com/carstencodes/simplepycons) <img src="https://cdn.simpleicons.org/python/000/fff" alt="Python" align=left width=24 height=24> | [@carstencodes](https://github.com/carstencodes) | ![License](https://img.shields.io/github/license/carstencodes/simplepycons?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fraw.githubusercontent.com%2Fcarstencodes%2Fsimplepycons%2Frefs%2Fheads%2Fmain%2F.gitmodules&search=%5C%5Bsubmodule%20%22vendor%5C%2Fsimple-icons%22%5C%5D%5B%5E%5C%5B%5D%2Bbranch%5Cs*%3D%5Cs*%28%3F%3Cversion%3E%5Cd%2B%5C.%5Cd%2B%5C.%5Cd%2B%3F%29&replace=%24%3Cversion%3E&flags=ims&label=) |
| [React package](https://github.com/icons-pack/react-simple-icons) <img src="https://cdn.simpleicons.org/react/000/fff" alt="React" align=left width=24 height=24> | [@wootsbot](https://github.com/wootsbot) | ![License](https://img.shields.io/github/license/icons-pack/react-simple-icons?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Ficons-pack%2Freact-simple-icons%2Fmain%2Fpackage.json&query=%24..%5B'simple-icons'%5D&label=) |
| [Ruby gem](https://rubygems.org/gems/simple-icons-rails) <img src="https://cdn.simpleicons.org/rubygems/000/fff" alt="Ruby" align=left width=24 height=24> | [@thepew](https://github.com/the-pew-inc) | ![License](https://img.shields.io/github/license/the-pew-inc/simple-icons-rails?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fsimple-icons%2Fsimple-icons%2Fmaster%2Fpackage.json&query=%24.version&label=) |
| [Rust crate](https://crates.io/crates/simpleicons-rs) <img src="https://cdn.simpleicons.org/rust/000/fff" alt="Rust" align=left width=24 height=24> | [@cscnk52](https://github.com/cscnk52) | ![License](https://img.shields.io/github/license/cscnk52/simpleicons-rs?label=) | ![Simple Icons version](https://img.shields.io/crates/v/simpleicons-rs?color=blue&label=) |
| [Svelte package](https://github.com/icons-pack/svelte-simple-icons) <img src="https://cdn.simpleicons.org/svelte/000/fff" alt="Svelte" align=left width=24 height=24> | [@wootsbot](https://github.com/wootsbot) | ![License](https://img.shields.io/github/license/icons-pack/svelte-simple-icons?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Ficons-pack%2Fsvelte-simple-icons%2Fmain%2Fpackage.json&query=%24..%5B'simple-icons'%5D&label=) |
| [Vue 3 package](https://github.com/wyatt-herkamp/vue3-simple-icons) <img src="https://cdn.simpleicons.org/vuedotjs/000/fff" alt="Vue" align=left width=24 height=24> | [@wyatt-herkamp](https://github.com/wyatt-herkamp) | ![License](https://img.shields.io/github/license/wyatt-herkamp/vue3-simple-icons?label=) | ![Simple Icons version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fwyatt-herkamp%2Fvue3-simple-icons%2Fmain%2Fsimple-icons.json&query=simpleIconsVersion&label=) |
Maintain a library? [Submit a PR][open-pr] to include it in the list above.
## Contribute
[![Good first issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Fsearch%2Fissues%3Fq%3Drepo%3Asimple-icons%2Fsimple-icons%2520label%3A%2522good%2520first%2520issue%2522%2520is%3Aopen%2520-linked%3Apr&query=%24.total_count&suffix=%20open&logo=github&label=good%20first%20issues&color=228f6c&labelColor=228f6c&logoColor=white&style=flat-square)](https://github.com/simple-icons/simple-icons/issues?q=is%3Aopen+label%3A%22good+first+issue%22+-linked%3Apr) [![Icon issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Fsearch%2Fissues%3Fq%3Drepo%3Asimple-icons%2Fsimple-icons%2520label%3A%2522update%2520icon%2Fdata%2522%2C%2522new%2520icon%2522%2520is%3Aopen%2520-linked%3Apr&query=%24.total_count&suffix=%20open&logo=svg&logoColor=333&label=icon%20issues&labelColor=FFB13B&color=FFB13B&style=flat-square)](https://github.com/simple-icons/simple-icons/issues?q=is%3Aissue+is%3Aopen+label%3A%22new+icon%22%2C%22update+icon%2Fdata%22) [![Code issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Fsearch%2Fissues%3Fq%3Drepo%3Asimple-icons%2Fsimple-icons%2520is%3Aissue%2520is%3Aopen%2520label%3Ameta%2Cpackage%2520-linked%3Apr&query=%24.total_count&suffix=%20open&logo=typescript&logoColor=white&label=code%20issues&labelColor=3178C6&color=3178C6&style=flat-square)](https://github.com/simple-icons/simple-icons/issues?q=is%3Aissue+is%3Aopen+label%3Adocs%2Cmeta%2Cpackage+-linked%3Apr) [![Documentation issues](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.github.com%2Fsearch%2Fissues%3Fq%3Drepo%3Asimple-icons%2Fsimple-icons%2520label%3Adocs%2520is%3Aopen%2520-linked%3Apr&query=%24.total_count&suffix=%20open&logo=markdown&label=docs%20issues&labelColor=343a40&color=343a40&logoColor=FFF&style=flat-square)](https://github.com/simple-icons/simple-icons/issues?q=is%3Aopen+is%3Aissue+label%3Adocs+-linked%3Apr)
Information describing how to contribute can be found in the file [CONTRIBUTING.md](https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md)
[slug]: https://github.com/simple-icons/simple-icons/blob/master/slugs.md
[open-pr]: https://github.com/simple-icons/simple-icons/compare
## Contributors
<a href="https://github.com/simple-icons/simple-icons/graphs/contributors">
<img
src="https://opencollective.com/simple-icons/contributors.svg?width=890&button=false"
alt="Contributors"
/>
</a>

View File

@@ -0,0 +1,21 @@
# Versioning
We use [Semantic Versioning](https://semver.org/) to version Simple Icons. In short, this means that version numbers are structured as `MAJOR.MINOR.PATCH`. For example, version `3.1.4` is major version `3`, minor version `1`, and patch `4`. Increasing each of these numbers implies certain kinds of changes.
For Simple Icons, given a change to the version number you can expect the following kinds of changes:
| Version number increase | Kinds of changes |
| :-- | :-- |
| _Major_ | Removed icons; Slug changed icons; Breaking API changes |
| _Minor_ | New icons; Title changed icons; API changes |
| _Patch_ | Updated SVGs; Updated metadata |
## Release Schedule
_Minor_ releases and _patches_ are scheduled on a weekly basis and are generally released on a Sunday.
_Major_ releases are scheduled on a half-year basis, mainly to remove old SVGs. That is, approximately every 6 months a normal Sunday release is a _major_ release instead of a _minor_ release or _patch_.
## Deprecation
After a _major_ release, the only other supported version is the last version of the previous _major_ release. This previous version will only receive bug fixes to either the npm library, or our internal APIs.

View File

@@ -0,0 +1,24 @@
{
"name": "simple-icons/simple-icons",
"description": "SVG icons for popular brands",
"homepage": "https://simpleicons.org/",
"keywords": [
"svg",
"icons"
],
"support": {
"issues": "https://github.com/simple-icons/simple-icons/issues",
"source": "https://github.com/simple-icons/simple-icons",
"docs": "https://github.com/simple-icons/simple-icons#php-usage-",
"chat": "https://discord.gg/vUXFa7t5xJ",
"forum": "https://github.com/simple-icons/simple-icons/discussions",
"security": "https://github.com/simple-icons/simple-icons/security/policy"
},
"license": "CC0-1.0",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/simple-icons"
}
]
}

View File

@@ -0,0 +1,59 @@
// @ts-check
/**
* @file Type definitions for icons JSON data.
*/
/**
* The data for a Simple Icon.
*/
export type IconData = {
title: string;
hex: string;
source: string;
slug: string;
guidelines?: string;
license?: Omit<SPDXLicense, 'url'> | CustomLicense;
aliases?: Aliases;
};
/**
* The aliases for a Simple Icon.
*
* Corresponds to the `aliases` property in the *data/simple-icons.json* file.
* @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#aliases Aliases}
*/
export type Aliases = {
aka?: string[];
dup?: DuplicateAlias[];
loc?: Record<string, string>;
old?: string[];
};
export type DuplicateAlias = {
title: string;
hex?: string;
guidelines?: string;
loc?: Record<string, string>;
};
/**
* The license for a Simple Icon.
* @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#optional-data Optional Data}
*/
export type License = SPDXLicense | CustomLicense;
// eslint-disable-next-line @typescript-eslint/naming-convention
export type SPDXLicense = {
type: string;
url: string;
};
export type CustomLicense = {
type: 'custom';
url: string;
};
declare const icons: IconData[];
export default icons;
export = icons;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>1001Tracklists</title><path d="M8.0957 1.334v1.3457H6.7461v1.3457H5.3984V5.371H4.0488v1.3457H2.6992v6.6816H1.3496v1.3477H0v2.4512h1.3496v1.3457h1.3496v1.3457h2.457v-7.836H3.8067V7.8223h1.3497V6.4766h1.3496V5.1309h1.3496V3.7852h8.289v1.3457h1.3496v1.3457h1.3496v1.3457h1.3497v4.2304h-1.3497v7.836h2.457V18.543h1.3497v-1.3457H24V14.746h-1.3496v-1.3477h-1.3496V6.7168h-1.3496V5.3711h-1.3496V4.0254h-1.3477V2.6797h-1.3496V1.334Zm1.3711 8v1.3515H8.1113v3.8165h2.4688v-4.0567h2.9512v4.3477h-1.3555v1.3515h-1.3535v2.4649h2.4668v-2.7051H16v-5.2188h-1.3555V9.334Zm1.3555 10.8691v2.463h2.4668v-2.463z"/></svg>

After

Width:  |  Height:  |  Size: 677 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>1&amp;1</title><path d="M0 0v24h24V0zm11.717 5.792c1.564 0 2.671 1.04 2.671 2.468 0 1.044-.428 1.819-1.746 2.915l1.952 2.648c.163-.147.303-1.046.274-1.777-.003-.087-.022-.341-.04-.62h1.814c0 .244.024.595.024.683 0 1.426-.224 2.327-.909 3.198L17.2 17.22h-2.232l-.503-.678c-.823.659-1.546.905-2.713.898-2.284-.013-3.857-1.173-4.005-3.239-.089-1.235.737-2.506 2.32-3.42C9.049 9.477 8.84 9.025 8.84 8.207c0-1.392 1.191-2.415 2.878-2.415zm-9.424.134h4.064v11.296H4.1V7.735H2.293zm14.45 0h4.065v11.296H18.55V7.735h-1.807zm-5.036 1.49c-.545 0-.931.358-.931.845 0 .47.14.726.79 1.562.772-.557 1.058-1.075 1.058-1.58 0-.504-.354-.828-.917-.828zm-.517 4.811c-1.002.663-1.404 1.31-1.386 1.919.03.928.806 1.522 1.948 1.522.703 0 1.174-.257 1.579-.594z"/></svg>

After

Width:  |  Height:  |  Size: 826 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>1.1.1.1</title><path d="M5.389 0A5.377 5.377 0 0 0 0 5.389V18.61A5.377 5.377 0 0 0 5.389 24H18.61A5.377 5.377 0 0 0 24 18.611V5.39A5.377 5.377 0 0 0 18.611 0Zm11.546 4.595h.942v3.122h.69v.868h-.69v1.201h-1.001V8.585H14.68v-.964zm-6.07.589h2.523v14.842h-3.094V9.79H6.68V7.805c.95-.042 1.616-.103 1.997-.184.606-.13 1.1-.39 1.48-.779.26-.266.457-.62.592-1.064.077-.267.116-.464.116-.594Zm5.989.73L15.513 7.72h1.365V5.915Z"/></svg>

After

Width:  |  Height:  |  Size: 506 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>1Panel</title><path d="m12 0 10.349 6v12L12 24 1.651 18V6zm0 .326L1.897 6.158v11.664L12 23.653l10.103-5.831V6.158zM8.84 20.523l-5.801-3.349V6.826L12 1.653l2.23 1.287-8.925 5.195v7.73l5.792 3.345zm6.299-17.058 5.822 3.361v10.348L12 22.347l-2.274-1.312 8.969-5.17v-7.73l-5.823-3.362zm-2.137 3.35v2.869l.024 7.666-.691.384-2.18-1.249.008-6.801H8.958L8.95 8.351l3.412-1.965z"/></svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>1Password</title><path d="M12 .007C5.373.007 0 5.376 0 11.999c0 6.624 5.373 11.994 12 11.994S24 18.623 24 12C24 5.376 18.627.007 12 .007Zm-.895 4.857h1.788c.484 0 .729.002.914.096a.86.86 0 0 1 .377.377c.094.185.095.428.095.912v6.016c0 .12 0 .182-.015.238a.427.427 0 0 1-.067.137.923.923 0 0 1-.174.162l-.695.564c-.113.092-.17.138-.191.194a.216.216 0 0 0 0 .15c.02.055.078.101.191.193l.695.565c.094.076.14.115.174.162.03.042.053.087.067.137a.936.936 0 0 1 .015.238v2.746c0 .484-.001.727-.095.912a.86.86 0 0 1-.377.377c-.185.094-.43.096-.914.096h-1.788c-.484 0-.726-.002-.912-.096a.86.86 0 0 1-.377-.377c-.094-.185-.095-.428-.095-.912v-6.016c0-.12 0-.182.015-.238a.437.437 0 0 1 .067-.139c.034-.047.08-.083.174-.16l.695-.564c.113-.092.17-.138.191-.194a.216.216 0 0 0 0-.15c-.02-.055-.078-.101-.191-.193l-.695-.565a.92.92 0 0 1-.174-.162.437.437 0 0 1-.067-.139.92.92 0 0 1-.015-.236V6.25c0-.484.001-.727.095-.912a.86.86 0 0 1 .377-.377c.186-.094.428-.096.912-.096z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>2FAS</title><path d="M12 0c-.918 0-1.833.12-2.72.355L4.07 1.748a2.64 2.64 0 0 0-1.96 2.547v9.115a7.913 7.913 0 0 0 3.552 6.606l5.697 3.765a1.32 1.32 0 0 0 1.467-.008l5.572-3.752a7.931 7.931 0 0 0 3.493-6.57V4.295a2.638 2.638 0 0 0-1.961-2.547L14.72.355A10.594 10.594 0 0 0 12 0ZM7.383 5.4h9.228c.726 0 1.32.594 1.32 1.32 0 .734-.587 1.32-1.32 1.32H7.383c-.727 0-1.32-.593-1.32-1.32 0-.726.593-1.32 1.32-1.32zM7.38 9.357h3.299c.727 0 1.32.595 1.32 1.32a1.32 1.32 0 0 1-1.318 1.32H7.38c-.726 0-1.32-.592-1.32-1.32 0-.725.594-1.32 1.32-1.32zm0 3.96c.727 0 1.32.593 1.32 1.32 0 .727-.586 1.318-1.32 1.318-.726 0-1.32-.592-1.32-1.318 0-.727.594-1.32 1.32-1.32z"/></svg>

After

Width:  |  Height:  |  Size: 742 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>2K</title><path d="M0 .002v23.997h24V.002H0Zm10.962 5.592c2.36 0 4.443.416 3.799 2.423-.434 1.365-2.017 1.918-3.114 2.109l-2.757.489c-.655.114-1.039.277-1.3.549h6.012l-.818 2.529 3.446-2.529h3.755l-4.091 2.772 2.07 4.402h-3.766l-1.082-2.754-1.197.826-.619 1.928H8.471l1.718-5.374h-6.25C4.874 10.2 6.891 9.36 8.731 8.989l2.264-.457c.387-.07.64-.259.736-.557.136-.416-.32-.581-.994-.581-.784 0-1.604.074-1.984 1.005H5.646c1.009-2.474 3.483-2.805 5.316-2.805Z"/></svg>

After

Width:  |  Height:  |  Size: 543 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>30 seconds of code</title><path d="M13.895 12c0 .734-.593 1.328-1.324 1.328h-.58a6.666 6.666 0 0 1-.389 5.09 6.633 6.633 0 0 1-2.22 2.508 6.6 6.6 0 0 1-9.028-1.619l-.044-.06c-.47-.563-.397-1.4.163-1.872a1.32 1.32 0 0 1 1.864.164l.158.207a3.972 3.972 0 0 0 3.523 1.63 3.96 3.96 0 0 0 3.226-2.163A3.997 3.997 0 0 0 7.771 12 3.977 3.977 0 0 0 9.45 9.927a3.997 3.997 0 0 0-1.537-4.644 3.96 3.96 0 0 0-5.417.971l-.158.208a1.32 1.32 0 0 1-1.864.163A1.332 1.332 0 0 1 .31 4.754l.044-.061a6.622 6.622 0 0 1 2.583-2.128 6.595 6.595 0 0 1 6.446.51 6.634 6.634 0 0 1 2.22 2.506 6.662 6.662 0 0 1 .389 5.09h.58c.73 0 1.323.595 1.323 1.329Zm-.067 5.21a8.28 8.28 0 0 1-1.196 2.811 6.628 6.628 0 0 0 4.717 1.964A6.654 6.654 0 0 0 24 15.328V8.672a6.654 6.654 0 0 0-6.651-6.657c-1.844 0-3.513.75-4.717 1.964a8.28 8.28 0 0 1 1.196 2.81 3.99 3.99 0 0 1 7.511 1.883v6.656a3.992 3.992 0 0 1-7.51 1.883Z"/></svg>

After

Width:  |  Height:  |  Size: 968 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>365 Data Science</title><path d="M1.4995 13.6448v-.8577h.8188c.5752 0 1.183-.152 1.1905-.6681 0-.3178-.2587-.7385-1.1666-.7385-.5024 0-1.183.1746-1.183.6832h-1.07c0-1.1993 1.2156-1.6326 2.2605-1.6326 1.07 0 2.2204.5086 2.2291 1.6916a1.1716 1.1716 0 0 1-.7861 1.0876c.5425.1746.9243.6995.9243 1.168 0 1.3588-1.3049 1.7581-2.3987 1.7581-1.0938 0-2.3108-.4345-2.3183-1.6979h1.0775c0 .5727.7862.7711 1.2483.7711.5426 0 1.2647-.2223 1.2647-.8188 0-.3328-.1871-.746-1.2081-.746zm5.0083-1.4304v.8339c.2512-.3768.8666-.5878 1.3124-.5878 1.1428 0 2.3422.4685 2.3422 1.8512 0 1.232-1.207 1.8436-2.3497 1.8436-1.1429 0-2.3673-.6116-2.3673-1.8587V12.222c0-1.2395 1.168-1.8184 2.3346-1.8184 1.232 0 2.3259.476 2.3259 1.6439H9.0422c0-.4923-.628-.7385-1.2396-.7385-.6405-.0075-1.2885.3341-1.2885.9369zm2.5934 2.0973c0-.6116-.5677-.9369-1.281-.9369-.7133 0-1.3212.3177-1.3124.9369.0088.6191.6079.908 1.3049.908.6718 0 1.2885-.2964 1.2885-.908zm5.9967-2.8119h-3.063v.977h1.1013c1.2082 0 2.2857.4848 2.2781 1.7872 0 1.2633-1.0787 1.8674-2.2216 1.8674-1.247 0-2.317-.437-2.3258-1.7406h1.0637c0 .6129.7133.771 1.2483.771.535 0 1.1742-.2385 1.1742-.8978.0088-.628-.6279-.81-1.2156-.81h-2.1488v-2.94h4.1092zm7.9169-3.6897a.9494.9494 0 0 0-.4559 1.7808l-1.9252 4.7183a.9444.9444 0 0 0-.7988.0829l-2.047-2.3422a.9432.9432 0 0 0-.089-1.27.9431.9431 0 0 0-1.4105 1.2412.943.943 0 0 0 1.2483.2498l2.0458 2.3422a.9518.9518 0 0 0-.2072.5927.9491.9491 0 0 0 .2654.6936.9493.9493 0 0 0 1.5701-.3198.9491.9491 0 0 0 .062-.3738.9432.9432 0 0 0-.3427-.7296l1.949-4.7723a.8264.8264 0 0 0 .137.0101.9493.9493 0 0 0 .9083-.5775.9493.9493 0 0 0-.2148-1.0547.9495.9495 0 0 0-.6936-.2654Z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>3M</title><path d="M18.903 5.954L17.17 13.03l-1.739-7.076h-5.099v2.613C9.72 6.28 7.56 5.706 5.558 5.674 3.12 5.641.563 6.701.469 9.936h3.373c0-.977.747-1.536 1.588-1.523 1.032-.008 1.508.434 1.533 1.124-.036.597-.387 1.014-1.525 1.014H4.303V12.9h1.03c.584 0 1.399.319 1.431 1.155.04.995-.652 1.435-1.501 1.443-1.517-.053-1.763-1.225-1.763-2.23H0c.015.677-.151 5.091 5.337 5.059 2.629.025 4.464-1.085 5.003-2.613v2.342h3.455v-7.632l1.867 7.634h3.018l1.875-7.626v7.634H24V5.954h-5.097zm-8.561 7.06c-.429-.893-1.034-1.284-1.376-1.407.714-.319 1.09-.751 1.376-1.614v3.021z"/></svg>

After

Width:  |  Height:  |  Size: 655 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>42</title><path d="M19.581 16.851H24v-4.439ZM24 3.574h-4.419v4.42l-4.419 4.418v4.44h4.419v-4.44L24 7.993Zm-4.419 0h-4.419v4.42zm-6.324 8.838H4.419l8.838-8.838H8.838L0 12.412v3.595h8.838v4.419h4.419z"/></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>4chan</title><path d="M11.07 8.82S9.803 1.079 5.145 1.097C2.006 1.109.78 4.124 3.055 4.802c0 0-2.698.973-2.698 2.697 0 1.725 4.274 3.54 10.713 1.32zm1.931 5.924s.904 7.791 5.558 7.991c3.136.135 4.503-2.82 2.262-3.604 0 0 2.74-.845 2.82-2.567.08-1.723-4.105-3.737-10.64-1.82zm-3.672-1.55s-7.532 2.19-6.952 6.813c.39 3.114 3.53 3.969 3.93 1.63 0 0 1.29 2.559 3.002 2.351 1.712-.208 3-4.67.02-10.794zm5.623-2.467s7.727-1.35 7.66-6.008c-.046-3.138-3.074-4.333-3.728-2.051 0 0-1-2.686-2.726-2.668-1.724.018-3.494 4.312-1.206 10.727z"/></svg>

After

Width:  |  Height:  |  Size: 614 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>4D</title><path d="M20.64 0v24H3.36V0h17.28zM10.49 11.827c-.115.138-5.882 6.789-5.983 6.9-.058.07-.187.194-.187.36 0 .153.187.208.36.208h4.4v-1.067H5.83c.49-.61 3.38-3.824 3.696-4.226v5.34c0 .194-.005.965-.043 1.602-.029.43-.13.637-.661.693-.23.027-.533.041-.662.041-.072 0-.115.083-.115.18 0 .097.072.167.23.167.777 0 1.539-.042 1.942-.042 1.236 0 2.646.097 3.178.097 2.618 0 4.099-.97 4.746-1.607.791-.776 1.539-2.093 1.539-3.81 0-1.622-.662-2.758-1.38-3.465-1.54-1.565-3.913-1.565-5.682-1.565-.56 0-1.035.027-1.064.027-.388.042-.345-.124-.59-.138-.158-.014-.258.055-.474.305zm1.898.443c1.108 0 2.719.166 4.027 1.372.604.554 1.367 1.676 1.367 3.408 0 1.414-.288 2.66-1.194 3.409-.849.706-1.812.984-3.265.984-1.122 0-1.683-.291-1.87-.54-.115-.153-.172-.694-.186-1.04 0-.097-.015-.29-.015-.568h1.021c.245 0 .317-.055.389-.18.1-.18.244-.735.244-.86 0-.11-.057-.166-.13-.166-.086 0-.273.139-.647.139h-.877v-5.584c0-.152.058-.222.173-.277.115-.056.676-.097.963-.097z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>500px</title><path d="M7.451 8.9995A3.0005 3.0005 0 1 0 10.4514 12a3.0275 3.0275 0 0 0-3.0006-3.0005Zm0 5.371A2.3554 2.3554 0 1 1 9.7912 12a2.3704 2.3704 0 0 1-2.3404 2.3704Zm6.448-5.371A3.0005 3.0005 0 1 0 16.8997 12a3.0005 3.0005 0 0 0-3.0005-3.0005Zm0 5.371A2.3554 2.3554 0 1 1 16.2396 12a2.3314 2.3314 0 0 1-2.3404 2.3704zM2.29 10.7997a2.0224 2.0224 0 0 0-1.5903.42V9.6297h2.7005c.09 0 .15-.03.15-.3 0-.2701-.12-.2701-.18-.2701H.3997a.27.27 0 0 0-.27.27V11.97c0 .15.09.18.24.21a.228.228 0 0 0 .27-.06A1.7073 1.7073 0 0 1 2.14 11.4 1.5603 1.5603 0 0 1 3.4902 12.72 1.5183 1.5183 0 0 1 2.17 14.4004h-.18a1.5303 1.5303 0 0 1-1.4103-.9901c-.03-.09-.09-.15-.33-.06-.2401.09-.2701.15-.2401.24a2.1274 2.1274 0 0 0 2.7005 1.2602A2.1274 2.1274 0 0 0 3.9703 12.15 2.1004 2.1004 0 0 0 2.29 10.7998zm16.65-1.7703a1.6263 1.6263 0 0 0-1.4403 1.6203v2.6704c0 .15.12.18.3.18s.3001-.03.3001-.18v-2.6704a1.0082 1.0082 0 0 1 .8702-1.0202.9872.9872 0 0 1 .7501.24.9572.9572 0 0 1 .33.7202 1.2002 1.2002 0 0 1-.21.57A.9452.9452 0 0 1 19 11.55c-.12 0-.21 0-.24.27 0 .1801 0 .2701.15.3001a1.4763 1.4763 0 0 0 .8701-.18 1.6113 1.6113 0 0 0 .8702-1.2602 1.5543 1.5543 0 0 0-1.4463-1.6803.8311.8311 0 0 1-.264.03zm3.9307 1.5602 1.0802-1.0801c.03-.03.12-.12-.06-.3301a.3.3 0 0 0-.2101-.12.156.156 0 0 0-.12.06l-1.0802 1.0802-1.0802-1.1102c-.09-.09-.18-.06-.33.06-.15.12-.15.24-.06.33l1.0801 1.0802-1.0862 1.1102a.228.228 0 0 0-.06.12.252.252 0 0 0 .12.2101.483.483 0 0 0 .21.12.318.318 0 0 0 .1501-.06l1.0802-1.0802 1.0802 1.0802a.156.156 0 0 0 .12.06.3.3 0 0 0 .21-.12c.09-.12.12-.24.03-.3z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>7Zip</title><path d="M0 18.858h24V8.181H10.717V5.142H0ZM2.021 7.271h6.657v1.994c-1.74 2.09-2.84 4.502-2.948 7.404H3.477c.09-2.501.353-4.954 2.283-6.994l.033-.033H2.021Zm8.45 1.253h13.215v10.143H10.47Zm6.01 1.213v6.871h1.482v-6.87Zm2.755.043v6.912h1.616v-2.42h1.029c.43-.001.754-.29.969-.716.427-.848.429-2.257-.024-3.092-.227-.419-.571-.697-1.033-.684zm-7.924.002v1.596h2.217l-2.304 3.736v1.54h4.287V15.1h-2.698l2.786-3.909v-1.41Zm9.452 1.512h.595c.164-.006.287.081.371.217.17.273.172.736.004.99a.364.364 0 0 1-.373.176l-.55.047z"/></svg>

After

Width:  |  Height:  |  Size: 616 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>99designs</title><path d="M21.6504 13.7786c0 1.163-.943 2.1059-2.1059 2.1059-1.163 0-2.1059-.943-2.1059-2.1059 0-1.163.943-2.1059 2.1059-2.1059 1.163 0 2.1059.943 2.1059 2.106zm-7.557-3.5718c0 1.0842-.8775 2.0229-2.0228 2.0229-1.117 0-2.0231-.9059-2.0231-2.0229s.906-2.0231 2.0231-2.0231c1.117 0 2.0229.906 2.0229 2.0231zm-7.6605 0c0 1.0822-.8759 2.0229-2.0231 2.0229-1.117 0-2.0228-.9059-2.0228-2.0229s.9058-2.0231 2.0228-2.0231 2.0231.906 2.0231 2.0231zm11.008 7.663c.9166.3985 2.2434.466 3.1223.0578.392-.182.7534-.4776 1.0847-.8858v.8776H24V6.0624h-2.4847v4.2717c-.707-.6853-1.4491-.9773-2.451-.9773-1.0589 0-1.9244.3524-2.5844.9162.0003-.0221.0006-.044.0006-.0662 0-2.435-1.9751-4.4098-4.4099-4.4098-1.6397 0-3.0704.8951-3.8305 2.2236C7.4803 6.692 6.0493 5.797 4.4098 5.797 1.9748 5.797 0 7.7718 0 10.2068c0 2.3312 1.81 4.2403 4.101 4.399L2.188 17.9193H5.057c1.061-1.8422 2.1222-3.6844 3.1831-5.5266.712 1.244 2.0124 2.1083 3.5216 2.213l-1.913 3.3136h2.8688l2.2372-3.8842c.0665 1.5842.868 3.1305 2.4863 3.8345"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>9GAG</title><path d="m17.279 21.008 5.193-2.995V5.992l-5.193-2.996C14.423 1.348 12.048 0 12 0c-.048 0-2.423 1.348-5.279 2.996L1.528 5.992v2.354l5.193 2.996c2.856 1.648 5.232 2.996 5.28 2.996.048 0 1.469-.797 3.157-1.772a229.633 229.633 0 0 1 3.097-1.772c.016 0 .027 1.096.027 2.437l-.002 2.436-3.076 1.772c-1.692.975-3.115 1.783-3.163 1.795-.048.013-1.471-.776-3.162-1.752-1.69-.976-3.113-1.775-3.161-1.775-.155 0-4.036 2.274-4.011 2.35.031.093 10.136 5.937 10.276 5.943.057.002 2.44-1.344 5.296-2.992ZM9.847 8.391c-1.118-.65-2.033-1.2-2.033-1.222 0-.071 4.06-2.376 4.186-2.376.125 0 4.186 2.305 4.186 2.376 0 .063-4.047 2.375-4.184 2.39-.068.007-1.037-.519-2.155-1.168Z"/></svg>

After

Width:  |  Height:  |  Size: 757 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>ABB</title><path d="M13.086 16.594v-4.427h3.035c.25.418.362.947.362 1.476 0 1.559-1.17 2.867-2.84 2.951zm-.279-4.455v4.455h-2.784v-4.455zm3.147-.278h-2.868V7.406h.668c1.086 0 1.949.863 1.949 1.949 0 .64-.334 1.225-.835 1.587.417.223.807.529 1.086.919m-3.147-4.455v4.455h-2.784V7.406zm7.796 9.188v-4.427h3.035c.251.418.362.947.362 1.476 0 1.559-1.169 2.867-2.84 2.951zm-.278-4.455v4.455h-2.784v-4.455zm3.146-.278h-2.868V7.406h.668c1.086 0 1.949.863 1.949 1.949 0 .64-.334 1.225-.835 1.587.418.223.808.529 1.086.919m-3.146-4.455v4.455h-2.784V7.406zM1.587 12.139h2.868v2.506H2.979l-.668 1.949H0zm2.868-4.733v4.455H1.671l1.587-4.455zm.278 7.239v-2.506h2.868l1.587 4.455H6.877l-.668-1.949zm2.784-2.784H4.733V7.406H5.93z"/></svg>

After

Width:  |  Height:  |  Size: 801 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Abbott</title><path d="M20.812 2.4H0v3.197h19.773V5.6a1.03 1.03 0 0 1 1.032 1.031v10.742l-.004.007a1.034 1.034 0 0 1-1.034 1.025H4.23c-.569 0-1.033-.46-1.033-1.033v-4.34c0-.57.464-1.032 1.033-1.032H17.6V8.803H3.188A3.185 3.185 0 0 0 0 11.99v6.423A3.188 3.188 0 0 0 3.188 21.6h17.624A3.187 3.187 0 0 0 24 18.412V5.587A3.186 3.186 0 0 0 20.812 2.4"/></svg>

After

Width:  |  Height:  |  Size: 432 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Abbvie</title><path d="M23.186 20.17c-1.533 0-2.14-.612-2.347-1.838l-.406-1.74c-.413.72-2.453 3.579-6.945 3.579H8.89C1.94 20.17 0 15.467 0 12c0-3.885 2.347-8.17 8.884-8.17h4.905c5.005 0 7.759 2.853 8.372 6.431.512 2.96 1.839 9.91 1.839 9.91zM13.076 6.378h-3.88c-4.698 0-6.231 2.965-6.231 5.623 0 2.653 1.533 5.618 6.236 5.618h3.875c4.904 0 6.236-3.065 6.236-5.618 0-2.246-1.231-5.618-6.236-5.618z"/></svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>AB Download Manager</title><path d="M10.98 0c-.235 0-.425.2-.425.446V8.92c0 .246-.19.446-.425.446h-.704c-.346 0-.547.41-.346.705l2.574 3.782c.17.25.522.25.692 0l2.574-3.782c.201-.295 0-.705-.346-.705h-.704a.436.436 0 0 1-.425-.446V.446A.436.436 0 0 0 13.02 0ZM7.387 1.562a.732.732 0 0 0-.405.161l-1.096.883a.77.77 0 0 0-.123 1.066.735.735 0 0 0 1.045.126l1.096-.882a.77.77 0 0 0 .123-1.067.738.738 0 0 0-.64-.287Zm9.346 0a.737.737 0 0 0-.64.287.77.77 0 0 0 .123 1.067l1.096.882c.323.26.79.204 1.045-.126a.77.77 0 0 0-.123-1.066l-1.096-.883a.733.733 0 0 0-.405-.16ZM3.039 6.036a.745.745 0 0 0-.675.48l-.512 1.326c-.15.39.037.831.42.985a.74.74 0 0 0 .965-.428l.512-1.325a.765.765 0 0 0-.419-.985.728.728 0 0 0-.291-.053Zm18.042 0a.738.738 0 0 0-.29.053.765.765 0 0 0-.42.985l.512 1.325a.74.74 0 0 0 .965.428.765.765 0 0 0 .42-.985l-.512-1.325a.745.745 0 0 0-.675-.481ZM0 12a12 12 0 0 0 .913 4.592 11.997 11.997 0 0 0 2.42 3.703c.684-.681 1.565-1 2.489-1.053.925-.055 1.893.156 2.745.532a8.497 8.497 0 0 0 6.866 0c1.702-.751 3.865-.843 5.234.52a11.997 11.997 0 0 0 2.42-3.702A12 12 0 0 0 24 12h-3.687c-1.301 0-2.315 1.094-2.813 2.296a6 6 0 0 1-11.087 0C5.915 13.094 4.902 12 3.601 12Zm5.875 7.769c-.727.04-1.407.275-1.957.727-.073.06-.14.119-.204.18a11.997 11.997 0 0 0 3.694 2.41 12 12 0 0 0 9.184 0 11.997 11.997 0 0 0 3.694-2.41 3.977 3.977 0 0 0-.204-.18c-1.173-.963-2.96-.892-4.436-.24a9.023 9.023 0 0 1-7.292 0c-.478-.211-1.418-.546-2.48-.487Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>About.me</title><path d="M3.158 9.897v4.131h.65v-.408c.23.297.577.483.961.483.768 0 1.332-.582 1.332-1.573 0-.967-.558-1.568-1.332-1.568-.372 0-.719.168-.96.49V9.897Zm10.285.322v.818h-.495v.563h.495v1.729c0 .501.26.774.769.774.297 0 .49-.087.607-.192l-.155-.496a.4.4 0 0 1-.285.112c-.186 0-.285-.155-.285-.36V11.6h.607v-.563h-.607v-.818zm-5.488.743c-.954 0-1.536.706-1.536 1.567 0 .855.582 1.574 1.536 1.574s1.537-.719 1.537-1.574c0-.86-.583-1.567-1.537-1.567m14.577 0c-.886 0-1.518.7-1.518 1.567 0 .948.663 1.574 1.567 1.574.47 0 .91-.155 1.214-.44l-.297-.427c-.217.216-.557.334-.855.334-.564 0-.898-.378-.948-.824H24v-.16c0-.942-.57-1.624-1.468-1.624m-4.576 0c-.459 0-.849.298-.979.477v-.402h-.65v2.991h.65v-2.093c.137-.192.403-.397.694-.397.354 0 .49.217.49.54v1.95h.65v-2.093c.13-.199.403-.397.694-.397.353 0 .495.217.495.54v1.95h.65v-2.161c0-.607-.315-.905-.86-.905-.453 0-.85.28-1.016.545-.1-.322-.372-.545-.818-.545m-16.55 0c-.477 0-.91.15-1.257.484l.272.452a1.2 1.2 0 0 1 .886-.384c.41 0 .7.21.7.557v.446c-.223-.254-.563-.384-.972-.384-.49 0-1.035.285-1.035.979 0 .656.551.99 1.035.99.396 0 .75-.142.972-.402v.328h.65V12.04c0-.799-.582-1.078-1.25-1.078m8.449.075v2.118c0 .607.322.948.966.948.47 0 .842-.235 1.053-.471v.396h.65v-2.991h-.65v2.1a.99.99 0 0 1-.762.39c-.372 0-.607-.149-.607-.613v-1.877zm12.67.458c.589 0 .83.434.85.787H21.69c.025-.36.285-.787.837-.787m-17.942.043c.514 0 .843.415.843.992 0 .582-.329.997-.843.997a.98.98 0 0 1-.774-.397v-1.189a.98.98 0 0 1 .774-.403m3.372 0c.558 0 .861.465.861.991 0 .533-.303.998-.86.998-.552 0-.862-.465-.862-.997 0-.527.31-.992.861-.992m-6.66 1.041c.279 0 .557.105.712.31v.458c-.155.204-.433.31-.712.31-.366 0-.644-.21-.644-.539 0-.322.278-.539.644-.539m14.269.65a.44.44 0 0 0-.434.428c0 .235.198.44.434.44a.445.445 0 0 0 .434-.44.44.44 0 0 0-.434-.428"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Some files were not shown because too many files have changed in this diff Show More