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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ All notable changes to `ichava/core` follow [Keep a Changelog](https://keepachan

### Changed

- **`icon-sets.json` records each pack's `visibility`** (`public` or `private`), synced by `bin/sync-icon-sets.php` from the repos API and kept when a response does not state it (F-02). Documented under *The pack catalogue* in `docs/tools/artisan-commands.md`. `tests/Unit/SyncIconSetsVisibilityTest.php`.
- **The generic Blade snippet uses a prefix this host registers**: the short one while `components.short_prefix` names one, `ichava-core` when it is off (`ComponentPrefixes::snippetPrefix()`).
- **`IconDriverInterface` states `SvgDriver`'s real API** (`render(IconData …)`, `load(string $path …)`), `SvgDriver` implements it, the provider binds it, and `IconRegistry`, `IconSvgReader` and `DirectoryIconDriver` depend on it (C-17). The interface's old directory methods (`all`, `has`, `setConfig`, `getConfig`, `setIconPath`, `getIconPath`) are gone from it; `DirectoryIconDriver` keeps them as its own API. `IconRegistry::driver()` returns the interface. `tests/Feature/IconDriverContractTest.php`.
- **`IconSetBuilder::cache(false)` turns the lookup cache off.** It set a flag nothing read. Caching stays on by default. `tests/Unit/IconSetBuilderCacheTest.php`.
- **`components.test_icons` has a reader**: `ComponentPrefixes::testIconsEnabled()`, false whenever `components.enabled` is. `tests/Feature/TestIconsComponentKeyTest.php`.
- **A pack's `config.defaults` applies wherever the pack renders** (P-06). The registry applies `config.defaults.class`, `.attributes`, `.stroke_width` (as `stroke-width`) and `.fallback` to the set it builds, so `<x-ichava::icon>`, the pack's own tag and `IconRegistry::render()` draw the same attributes. Before, a pack's defaults lived in its `IconComponent` subclass and reached its own tag only. `tests/Feature/PackConfigDefaultsTest.php`.
- **The pack base providers live in `Providers\`** (C-07): `Support\IconPackServiceProvider` is `Providers\IconPackServiceProvider`, and `Support\ServiceProvider` is `Providers\PackServiceProvider`. No alias is left at the old names; a pack changes one `use` line. `tests/Unit/ProvidersNamespaceTest.php`.
- **`custom-icons.sets` keys take effect** (C-09): `enabled`, `prefix`, `default_class`, `default_attributes`, `fallback`, `variants`, `default_variant`, `supports_categories`, `blade_namespace`, `blade_component`, plus `package` for a directory without a `config.json` and `bulk`/`only` for several sets. Before, only `path` was read. `tests/Feature/CustomIconSetsTest.php`.
Expand All @@ -70,6 +75,7 @@ All notable changes to `ichava/core` follow [Keep a Changelog](https://keepachan

### Removed

- **`IchavaServiceProvider::isIchavaQueueCommand()`**, unused since C-01 moved the runtime limits into `RuntimeLimits`. **`IconSetBuilder::$discoveredIcons` and `$initialized`**, which nothing wrote or read.
- **Forward-only renames, with no alias left behind** (owner, 2026-11-05): `Support\IconPackServiceProvider` and `Support\ServiceProvider` (now `Providers\IconPackServiceProvider`, `Providers\PackServiceProvider`), `Actions\CountSvgFiles` (now `Support\SvgFileCounter`), `ClearDiscoveryCaches::generation()` (now `DiscoveryCacheGeneration::current()`), and `__invoke()` on four actions (now `handle()`). Every caller in the estate moves in the same sweep; each is recorded in the deprecations audit.
- **The bare `icon` morph alias** (now `ichava-icon`; the migration moves stored rows).
The estate is forward-only (owner, 2026-11-05: "never support legacy only forward"). Nothing has been released, so these compatibility paths go rather than stay beside their replacements.
Expand Down
24 changes: 23 additions & 1 deletion bin/sync-icon-sets.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
*
* For every set pinned in icon-sets.json (key/package/repository) this
* script resolves the latest release tag, reads the pack's config.json at
* that tag, counts the committed SVGs (in all, and per kind as icon_counts)
* that tag, counts the committed SVGs (in all, and per kind as icon_counts),
* records the repository's GitHub visibility (public or private)
* via the git-trees API, and writes
* the snapshot back — so `ichava:install` stays fully local with zero
* runtime network calls.
Expand Down Expand Up @@ -144,6 +145,7 @@ function snapshot(string $repository, string $configPath, array $previous): ?arr
}

$variants = $config['metadata']['data']['variants'] ?? null;
$visibility = visibilityOf(getJson("https://api.github.com/repos/{$repository}"), $previous['visibility'] ?? null);
$tree = $tag !== null ? svgPaths($repository, $tag, $configPath) : null;
$count = $tree !== null ? count($tree) : null;
$counts = $tree !== null ? countsByKind($tree, $config) : null;
Expand All @@ -156,9 +158,29 @@ function snapshot(string $repository, string $configPath, array $previous): ?arr
'icon_counts' => $counts ?? ($previous['icon_counts'] ?? null),
'variants' => is_array($variants) ? array_keys($variants) : ($previous['variants'] ?? null),
'latest_version' => $tag !== null ? ltrim($tag, 'vV') : ($previous['latest_version'] ?? null),
'visibility' => $visibility,
];
}

/**
* The repository's GitHub visibility, `public` or `private`, from the repos
* API response. A response that does not say (a failed request, or a private
* repository the token cannot see, which answers 404) keeps the previous
* value, so one bad night never flips a pack's recorded visibility.
*
* @param array<array-key, mixed>|null $repository
*/
function visibilityOf(?array $repository, mixed $previous): ?string
{
$visibility = is_array($repository) ? ($repository['visibility'] ?? null) : null;

if (in_array($visibility, ['public', 'private'], true)) {
return $visibility;
}

return in_array($previous, ['public', 'private'], true) ? $previous : null;
}

function latestTag(string $repository): ?string
{
$tags = getJson("https://api.github.com/repos/{$repository}/tags?per_page=100");
Expand Down
2 changes: 2 additions & 0 deletions config/ichava-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@
// beside the canonical `<x-ichava-icon-sets-tabler::icon>`.
'short_prefix' => env('ICHAVA_COMPONENT_SHORT_PREFIX', 'ichava'),

// The bundled <x-ichava::ichava-test-icons> component, registered by
// ichava/icon-browser when this is on (ComponentPrefixes::testIconsEnabled()).
'test_icons' => env('ICHAVA_TEST_COMPONENT_ENABLED', env('APP_ENV') === 'local'),
],

Expand Down
4 changes: 4 additions & 0 deletions docs/tools/artisan-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ php artisan ichava::ichava-core.watch --package=vendor/pack # Rescan one pack
php artisan ichava::ichava-core.watch --refresh # Record every pack's fingerprint after the scan
```

### The pack catalogue, `icon-sets.json`

`ichava::ichava-core.install` offers the packs listed in `icon-sets.json` at the root of core, and makes no network call. `bin/sync-icon-sets.php` refreshes it daily (`.github/workflows/sync-icon-sets.yml`). Each entry pins `key`, `package` and `repository`; the script syncs `title`, `description`, `license`, `icon_count`, `icon_counts` (per kind), `variants` and `latest_version` from the pack's `config.json` at its latest tag, and `visibility`, the repository's GitHub visibility (`public` or `private`), from the repos API. A response that does not state the visibility keeps the recorded value. A consumer that needs to know which packs are public reads `visibility` here rather than probing GitHub.

### Scaffolding a new pack moved out of core

`ichava::ichava-core.make:icon-package` was removed in core `0.3.0`. It lives in
Expand Down
2 changes: 1 addition & 1 deletion docs/tools/blade-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Both extend `IconComponent` and accept the same `name`, `class`, `title`, `aria`
<x-ichava::ichava-ui-icons name="search" class="w-5 h-5" />
```

Both are registered by `ichava/icon-browser`, and its `test_component_enabled` key turns the test component off. Core's own `components.test_icons` key is read by nothing today; it is kept, and listed for a decision.
Both are registered by `ichava/icon-browser`. Whether the test component is registered is core's `components.test_icons` key (`ICHAVA_TEST_COMPONENT_ENABLED`, on by default only when `APP_ENV` is `local`), read through `ComponentPrefixes::testIconsEnabled()`; `components.enabled` false turns it off with every other core tag.

### Per-package components

Expand Down
14 changes: 9 additions & 5 deletions icon-sets.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": "3.0",
"_note": "Static snapshot refreshed daily by .github/workflows/sync-icon-sets.yml (bin/sync-icon-sets.php). Each set pins key/package/repository; title, description, license, icon_count, icon_counts (per kind: icon, illustration), variants and latest_version are synced from the pack’s config.json at its latest GitHub tag. To add a set, append an entry carrying at least key, package, repository, title, icon_count and variants — IconSetCatalogService rejects any set missing one of those six, treating an empty string or empty array as missing, so a set pinned with only the first three makes ichava::ichava-core.install throw until the next nightly sync fills the rest. Take the values from the pack’s own config.json and let the workflow refresh them. Write with JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, as this file is generated and any other encoding reflows it. ichava::ichava-core.install reads this file only — zero runtime network calls.",
"_note": "Static snapshot refreshed daily by .github/workflows/sync-icon-sets.yml (bin/sync-icon-sets.php). Each set pins key/package/repository; title, description, license, icon_count, icon_counts (per kind: icon, illustration), variants, latest_version and visibility (the repository's GitHub visibility, public or private) are synced from the pack’s config.json at its latest GitHub tag. To add a set, append an entry carrying at least key, package, repository, title, icon_count and variants — IconSetCatalogService rejects any set missing one of those six, treating an empty string or empty array as missing, so a set pinned with only the first three makes ichava::ichava-core.install throw until the next nightly sync fills the rest. Take the values from the pack’s own config.json and let the workflow refresh them. Write with JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, as this file is generated and any other encoding reflows it. ichava::ichava-core.install reads this file only — zero runtime network calls.",
"config_path": "resources/assets/svg/config.json",
"sets": [
{
Expand All @@ -19,7 +19,8 @@
"icon_counts": {
"icon": 6238,
"illustration": 0
}
},
"visibility": "public"
},
{
"key": "flags",
Expand All @@ -37,7 +38,8 @@
"icon_counts": {
"icon": 542,
"illustration": 0
}
},
"visibility": "public"
},
{
"key": "emoji",
Expand All @@ -54,7 +56,8 @@
"icon_counts": {
"icon": 3435,
"illustration": 0
}
},
"visibility": "public"
},
{
"key": "openmoji",
Expand All @@ -71,7 +74,8 @@
"icon_counts": {
"icon": 8568,
"illustration": 0
}
},
"visibility": "public"
}
],
"synced_at": "2026-11-03T10:18:20Z"
Expand Down
64 changes: 24 additions & 40 deletions src/Contracts/IconDriverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,39 @@

namespace Simtabi\Laranail\Ichava\Contracts;

use Simtabi\Laranail\Ichava\Data\IconData;
use Simtabi\Laranail\Ichava\Drivers\SvgDriver;
use Simtabi\Laranail\Ichava\Exceptions\IchavaException;

/**
* Reads SVG files safely and renders icons from them.
*
* Bound to {@see SvgDriver}. The registry, the served-SVG reader and the
* directory driver depend on this, so a host substitutes the driver (a
* remote store, an encrypted one) without subclassing. The signatures are
* SvgDriver's: until 2026-11-05 this interface described a directory API no
* driver in use implemented (C-17).
*/
interface IconDriverInterface
{
/**
* Get all available icons
* Render $icon with $attributes applied to its root element.
*
* @return array<array-key, mixed>
*/
public function all(): array;

/**
* Render an icon as SVG string
*
* @param string $name Icon name
* @param array<string, mixed> $attributes HTML attributes
*/
public function render(string $name, array $attributes = []): string;

/**
* Check if an icon exists
*
* @param string $name Icon name
*/
public function has(string $name): bool;

/**
* Set configuration for the driver
*
* @param array<string, mixed> $config
* @param array<string, mixed> $attributes
* @param string|null $baseDir the pack directory the file must stay inside
* @param int|null $maxBytes the size ceiling for the icon's kind; null keeps the global one
*
* @return $this
* @throws IchavaException when the file is refused or cannot be read
*/
public function setConfig(array $config): static;
public function render(IconData $icon, array $attributes = [], ?string $baseDir = null, ?int $maxBytes = null): string;

/**
* Get configuration
* The raw, unsanitised content of the SVG at $path, after the safety
* checks: no symlink, contained in $baseDir, within the size bounds.
*
* @return array<string, mixed>
*/
public function getConfig(): array;

/**
* Set the icon path
* @param array<string, mixed> $options reserved for future loaders
*
* @return $this
*/
public function setIconPath(string $path): static;

/**
* Get the icon path
* @throws IchavaException when the file is refused or cannot be read
*/
public function getIconPath(): string;
public function load(string $path, array $options = [], ?string $baseDir = null, ?int $maxBytes = null): string;
}
18 changes: 6 additions & 12 deletions src/Drivers/DirectoryIconDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,20 @@
use Simtabi\Laranail\Ichava\Contracts\IconDriverInterface;

/**
* `IconDriverInterface` over one directory of SVG files.
*
* The extension point for a driver author: list, check and render icons by
* their path under the directory (`ui/check` → `ui/check.svg`), with no
* registry, pack or database involved. Rendering goes through SvgDriver, so
* the same containment checks, size limit and sanitiser apply.
*
* SvgDriver itself does not implement the interface: its `render()` takes a
* resolved IconData, which is what IconRegistry hands it, and changing that
* signature would break the registry and every subclass.
* Lists, checks and renders the icons in one directory of SVG files, by their
* path under it (`ui/check` → `ui/check.svg`), with no registry, pack or
* database involved. Rendering goes through the bound IconDriverInterface
* (SvgDriver), so the same containment checks, size limit and sanitiser apply.
*/
final class DirectoryIconDriver implements IconDriverInterface
final class DirectoryIconDriver
{
/** @var array<string, mixed> */
private array $config = [];

private string $iconPath = '';

public function __construct(
private readonly SvgDriver $driver,
private readonly IconDriverInterface $driver,
) {}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/Drivers/SvgDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Simtabi\Laranail\Ichava\Constants\IchavaConstants;
use Simtabi\Laranail\Ichava\Services\IconCacheService;
use Simtabi\Laranail\Ichava\Exceptions\IchavaException;
use Simtabi\Laranail\Ichava\Contracts\IconDriverInterface;
use Simtabi\Laranail\Ichava\Services\SvgProcessingService;

/**
Expand All @@ -37,7 +38,7 @@
* @see IconCacheService
* @see IchavaConstants
*/
final class SvgDriver
final class SvgDriver implements IconDriverInterface
{
use ContainsFilePaths;

Expand Down
20 changes: 2 additions & 18 deletions src/Providers/IchavaServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Simtabi\Laranail\Ichava\Providers;

use Throwable;
use Illuminate\Support\Str;
use Laravel\Horizon\Horizon;
use Illuminate\Support\Facades\DB;
use Simtabi\Laranail\Ichava\Ichava;
Expand Down Expand Up @@ -53,6 +52,7 @@
use Simtabi\Laranail\Ichava\Contracts\DatabaseOperations;
use Simtabi\Laranail\Ichava\Support\IchavaSessionManager;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Simtabi\Laranail\Ichava\Contracts\IconDriverInterface;
use Simtabi\Laranail\Ichava\Services\ConfigurationService;
use Simtabi\Laranail\Ichava\Services\IconDiscoveryService;
use Simtabi\Laranail\Ichava\Services\SvgProcessingService;
Expand Down Expand Up @@ -195,6 +195,7 @@ public function registeringPackage(): void

$this->app->singleton(PathResolver::class);
$this->app->singleton(SvgDriver::class);
$this->app->bind(IconDriverInterface::class, fn (Container $app): IconDriverInterface => $app->make(SvgDriver::class));

// 3. Cache service (depends on IchavaLogger only)
$this->app->singleton(IconCacheService::class);
Expand Down Expand Up @@ -645,21 +646,4 @@ protected function configureRuntimeSettings(): RuntimeConfigurator
{
return $this->app->make(RuntimeLimits::class)->configurator();
}

/**
* Check if we're running an Ichava queue command.
*/
protected function isIchavaQueueCommand(): bool
{
if (! $this->app->runningInConsole()) {
return false;
}

$argv = $_SERVER['argv'] ?? [];
$argv = is_array($argv) ? array_map(Coerce::string(...), $argv) : [];
$isQueueCommand = collect($argv)->contains(fn (string $arg) => Str::contains($arg, 'queue:'));
$isIchavaQueue = collect($argv)->contains(fn (string $arg) => Str::contains($arg, 'ichava'));

return $isQueueCommand && $isIchavaQueue;
}
}
8 changes: 6 additions & 2 deletions src/Services/IconBrowserService.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Simtabi\Laranail\Ichava\Models\IconTerm;
use Simtabi\Laranail\Ichava\Support\Helpers;
use Simtabi\Laranail\Ichava\Data\VariantData;
use Simtabi\Laranail\Ichava\View\ComponentPrefixes;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Simtabi\Laranail\Ichava\Exceptions\IchavaException;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
Expand All @@ -37,6 +38,7 @@ public function __construct(
private IconCacheService $cacheManager,
private IchavaLogger $logger,
private DatabaseOperations $database,
private ComponentPrefixes $prefixes,
) {}

/**
Expand Down Expand Up @@ -149,7 +151,9 @@ public function transformIcon(Icon $icon): array
* `<x-icon-sets-tabler-icon name="a-b" category="outline" />`, with the
* folders in `category` because the pack component reads a name containing
* a slash as a full path. Falls back to the generic form otherwise.
* - Generic: `<x-ichava::icon name="ichava/icon-sets-tabler::outline/a-b" />`.
* - Generic: `<x-ichava::icon name="ichava/icon-sets-tabler::outline/a-b" />`,
* under the short prefix while `components.short_prefix` names one and
* `ichava-core` when it is off, so the copied tag renders on this host.
* It used to be `<x-ichava-icon name="icon-sets-tabler:outline/a-b" />`, a
* tag nothing registers holding a reference the registry refuses.
*/
Expand All @@ -159,7 +163,7 @@ public function generateBladeComponent(Icon $icon, bool $useCleanSyntax = true,
$tag = $useCleanSyntax ? $this->packComponentTag($icon) : null;

if ($tag === null) {
return "<x-ichava::icon name=\"{$path}\" class=\"{$class}\" />";
return "<x-{$this->prefixes->snippetPrefix()}::icon name=\"{$path}\" class=\"{$class}\" />";
}

$relative = substr($path, strpos($path, '::') + 2);
Expand Down
Loading
Loading