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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 68 additions & 3 deletions .vite/vitePluginHugeicons.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type {Plugin} from 'vite';
import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs';
import {existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync} from 'node:fs';
import {resolve} from 'node:path';

// Bump this when the generated file template changes to force regeneration.
const PLUGIN_VERSION = '1';
const PLUGIN_VERSION = '3';

type IconData = readonly (readonly [string, Record<string, string | number>])[];

Expand Down Expand Up @@ -43,6 +43,65 @@ async function loadAllIcons(): Promise<Map<string, IconData>> {
return icons;
}

// Some @hugeicons/core-free-icons exports differ only in casing (e.g. BarCodeIcon vs
// BarcodeIcon). Written as separate .svelte files, those collide into a single file on
// case-insensitive file systems (macOS, Windows), so keep only one per case-insensitive name.
//
// The package also re-exports many icons under a second, unrelated name that points at the
// exact same array object (e.g. `exports.BarCodeIcon = BarCode01Icon`) — a deliberate
// "shorthand alias" convention used package-wide, not specific to the casing collisions.
// When a casing collision involves one of these aliases, keep the name that is the *only*
// reference to its artwork rather than the alias, so we don't discard the one file that
// actually renders that icon's unique design in favor of a redundant duplicate.
function dedupeCaseInsensitive(icons: Map<string, IconData>): Map<string, IconData> {
const namesByData = new Map<IconData, string[]>();
for (const [name, data] of icons) {
const names = namesByData.get(data) ?? [];
names.push(name);
namesByData.set(data, names);
}
const isRedundantAliasElsewhere = (name: string, data: IconData): boolean =>
namesByData.get(data)!.some(other => other !== name);

const deduped = new Map<string, IconData>();
const seenLowerCase = new Map<string, string>();
const skipped: string[] = [];

for (const name of [...icons.keys()].sort()) {
const lowerCaseName = name.toLowerCase();
const data = icons.get(name)!;
const existingName = seenLowerCase.get(lowerCaseName);

if (existingName === undefined) {
seenLowerCase.set(lowerCaseName, name);
deduped.set(name, data);
continue;
}

const existingData = deduped.get(existingName)!;
const existingIsAlias = isRedundantAliasElsewhere(existingName, existingData);
const currentIsAlias = isRedundantAliasElsewhere(name, data);

if (existingIsAlias && !currentIsAlias) {
deduped.delete(existingName);
deduped.set(name, data);
seenLowerCase.set(lowerCaseName, name);
skipped.push(`${existingName} (kept ${name})`);
} else {
skipped.push(`${name} (kept ${existingName})`);
}
}

if (skipped.length > 0) {
console.warn(
`[vite-plugin-hugeicons] Skipped ${skipped.length} icon(s) whose name differs only in ` +
`casing from another icon (would collide on case-insensitive file systems): ${skipped.join(', ')}`
);
}

return deduped;
}

function stripReactKey(data: IconData): unknown[][] {
return data.map(([tag, attrs]) => {
const {key: _key, ...rest} = attrs as Record<string, unknown>;
Expand All @@ -66,7 +125,13 @@ export function vitePluginHugeicons(outputDir: string): Plugin {
if (!needsRegeneration(outputDir)) return;

mkdirSync(outputDir, {recursive: true});
const icons = await loadAllIcons();
for (const entry of readdirSync(outputDir)) {
if (entry.endsWith('.svelte')) {
rmSync(resolve(outputDir, entry));
}
}

const icons = dedupeCaseInsensitive(await loadAllIcons());

for (const [name, data] of icons) {
writeFileSync(resolve(outputDir, `${name}.svelte`), generateSvelteFile(data), 'utf-8');
Expand Down
11 changes: 10 additions & 1 deletion _changelog/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@

### Internals

[//]: # (- Changes that are mostly relevant to maintainers and contributors, such as refactors, dependency updates, CI changes, etc.)
- Introduced a **kernel + extension architecture** for the frontend: `HawkiApp` is now assembled at startup from independent, ordered extensions (config, HTTP client, plugins, migrations, localization, modules, routing, stores, shell, legacy snippets/toast) instead of one bootstrap script. Each extension contributes typed properties to `app.*` via declaration merging. See `_documentation/600-Frontend/600-Advanced/110-App-and-Kernel.md`.
- Added a **plugin system** (`resources/js/kernel/plugins/`): a `HawkiPlugin` registers stores, resource/config schemas, modules, routes, and (for core plugins) migrations through lifecycle hooks, and is auto-discovered via `import.meta.glob('$lib/plugins/**/*.plugin.ts')`. All existing first-party frontend features were consolidated into a single `core` plugin at `resources/js/plugins/core/`. Third-party/runtime-installed plugins are not supported yet.
- Added a **feature module system** (`resources/js/kernel/modules/`): a `HawkiModule` bundles a localizable title/description/icon, namespaced routes, and an optional sidebar component behind one registered name (`${plugin}:${module}`). The chat feature is now registered as the `core:chat` module.
- The client-side **router is now actually wired up** (previously inert scaffolding, see `_documentation/600-Frontend/600-Advanced/200-Routing.md`): `RouterView`/`RouteView` resolve `universal-router` routes through pluggable strategies (`path`, `hash`, `transient`). Currently only reachable via new placeholder-only preview routes (`/new`, `/new/{slug}`) added to `routes/web.php`, in preparation for the planned HAWKI v3.0.0 single-page-app rewrite — the main app UI is unaffected.
- Added a `Shell` component + `ShellExtension` that mounts the Svelte app shell as soon as the DOM is ready (showing a loading indicator) while the rest of the kernel finishes booting in the background.
- New **hook-based access pattern** for components — `useApp()`, `useConfig()`, `useConnection()`, `useStore()`, `useTranslator()`, `useApi()` (`resources/js/app/hooks/`) — replacing direct imports of global singletons such as the old `utils/translator.ts` `__()` export and `components/app/AppContext.svelte.ts`.
- Large-scale reorganization of `resources/js/`: `data/`, `stores/`, `schemas/`, `encryption/`, and `oldUi/` moved under the new `kernel/`, `plugins/core/`, and `legacy/` directory structure; chat composer components moved to `resources/js/plugins/core/modules/chat/`; composer state "aspects" renamed to "slices" (`AttachmentSlice`, `GuardSlice`, `ModeSlice`, `ModelParameterSlice`, `ModelSlice`, `ModelUsageSlice`, `ToolSlice`).
- Added `universal-router` as a new frontend dependency and a `$plugins` path alias (Vite + `tsconfig.json`); `tsconfig.json` now uses `moduleResolution: "bundler"` and `skipLibCheck: true`; the `check` npm script now runs `svelte-check` with an explicit config and a larger Node heap (`--max-old-space-size=8192`) to avoid out-of-memory crashes during type checking, and a standalone `tsc` script was added.
- Expanded JSDoc usage examples and rationale comments across the UI primitive component library (`Badge`, `Button`, `Dialog`, `DropdownMenu*`, `Citation*`, `ToastContext`, and others). `Button` also gained an `accent` variant and automatic icon-only sizing when only `iconLeft`/`iconRight` are given without children; the `lg` and standalone `icon` size options were removed.
- Added new architecture documentation: "The App & Kernel", "Writing an Extension", "Writing a Plugin", and "Routing" (marked not-yet-fully-active), plus updates to the Contributing, Stores, Translations, and Old UI Integration docs.

### Deprecation

Expand Down
24 changes: 16 additions & 8 deletions _documentation/400-Contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,14 +169,22 @@ There is currently no automated frontend test suite. Frontend testing will be in

> **Planned Svelte rewrite:** The HAWKI frontend is being progressively migrated to a Svelte 5 SPA. **Do not add new code to the legacy vanilla-JS layer** (`public/js/`). All new frontend work must follow the patterns in the [Frontend documentation](600-Frontend/index.md).

| Topic | Document |
|---------------------------------------------|-------------------------------------------------------------------|
| Tech stack, directory structure | [Svelte Frontend](600-Frontend/100-Svelte-Frontend.md) |
| Component authoring conventions | [Writing Svelte Components](600-Frontend/400-Components/index.md) |
| CSS tokens, cascade layers, dark mode | [Styling](600-Frontend/200-Styling.md) |
| Config, API fetch helpers, resource schemas | [Data Layer](600-Frontend/300-Data/index.md) |
| `__()` translation function | [Translations](600-Frontend/500-Utilities/100-Translations.md) |
| Available UI primitive components | [UI Primitives](600-Frontend/400-Components/100-UI-Primitives.md) |
The frontend is a custom kernel + plugin system assembled from extensions. Before adding a new app-wide subsystem or feature module, read the architecture pages so your change lands in the right layer.

| Topic | Document |
|---|---|
| Tech stack, directory structure, snippet mounting | [Svelte Frontend](600-Frontend/100-Svelte-Frontend.md) |
| App assembly, extensions, declaration merging, `app.*` surface | [The App & Kernel](600-Frontend/600-Advanced/110-App-and-Kernel.md) |
| Boot stages and where each extension registers work | [App Startup](600-Frontend/600-Advanced/100-App-Startup.md) |
| Adding a new app-wide subsystem (extension) | [Writing an Extension](600-Frontend/600-Advanced/120-Writing-an-Extension.md) |
| Adding a feature (stores, schemas, snippets, modules) | [Writing a Plugin](600-Frontend/600-Advanced/130-Writing-a-Plugin.md) |
| Component authoring conventions | [Writing Svelte Components](600-Frontend/400-Components/index.md) |
| CSS tokens, cascade layers, dark mode | [Styling](600-Frontend/200-Styling.md) |
| Config, API fetch helpers, resource schemas, hooks | [Data Layer](600-Frontend/300-Data/index.md) |
| Reactive stores and `useStore()` | [Stores](600-Frontend/300-Data/100-Stores.md) |
| Translations and `useTranslator()` | [Translations](600-Frontend/500-Utilities/100-Translations.md) |
| Available UI primitive components | [UI Primitives](600-Frontend/400-Components/100-UI-Primitives.md) |
| Bridging new Svelte code to the legacy JS layer | [Old UI Integration](600-Frontend/600-Advanced/300-Old-Ui.md) |

---

Expand Down
Loading