@hzblj/kara-expo-plugin 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +5 -2
- package/build/index.d.ts +6 -5
- package/build/index.js +5 -20
- package/package.json +5 -6
- package/build/cli.d.ts +0 -5
- package/build/cli.js +0 -41
- package/build/icons.d.ts +0 -58
- package/build/icons.js +0 -152
- package/build/with-icons.d.ts +0 -26
- package/build/with-icons.js +0 -99
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @hzblj/kara-expo-plugin
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#31](https://github.com/hzblj/kara-ui/pull/31) [`4b20462`](https://github.com/hzblj/kara-ui/commit/4b204621670b6f640fc04bffa527007dda9e06a8) Thanks [@hzblj](https://github.com/hzblj)! - Icons and the zoom transition now come from two standalone packages, which work without kara too.
|
|
8
|
+
|
|
9
|
+
- **Icons: [@hzblj/expo-native-icons](https://www.npmjs.com/package/@hzblj/expo-native-icons).** The config plugin is the same (`plugins: [['@hzblj/kara-ui', {icons: {source: './assets/icons'}}]]`, the same options, `npx kara-icons` still works). The names change: the declaration is now `native-icons.d.ts` with the global `NativeIcons` (it was `kara-icons.d.ts` and `KaraIcons`), and each icon is `native_icon_<name>` natively (it was `kara_icon_<name>`). Delete the old `kara-icons.d.ts`, and run `expo prebuild --clean` once so the old assets go. `RnIcon` is now the package's `NativeIcon`: on Android it draws the drawable itself instead of hosting an `ImageView`, and remembers each icon's resource lookup, so a list of rows does less work. A glyph with a non-square viewBox no longer stretches on Android.
|
|
10
|
+
- **Zoom transition: [@hzblj/expo-zoom-transition](https://www.npmjs.com/package/@hzblj/expo-zoom-transition).** `ZoomTransition` and its helpers from `@hzblj/kara-ui/router` are unchanged: kara's tokens over the package. The native code moved out of `@hzblj/kara-native` into the package's own module, which `@hzblj/kara-native` depends on, so autolinking picks it up. A tapped context menu preview still zooms. Rebuild the native app.
|
|
11
|
+
- `iconResourceName` moved from `@hzblj/kara-native` to `@hzblj/expo-native-icons`, and `@hzblj/kara-expo-plugin` no longer exports the icon generator or `./cli`: import them from `@hzblj/expo-native-icons/plugin` and `@hzblj/expo-native-icons/cli`.
|
|
12
|
+
|
|
3
13
|
## 0.12.0
|
|
4
14
|
|
|
5
15
|
## 0.11.0
|
package/README.md
CHANGED
|
@@ -23,9 +23,12 @@ plugins: [['@hzblj/kara-ui', {icons: {source: './assets/icons'}}]]
|
|
|
23
23
|
## What's inside
|
|
24
24
|
|
|
25
25
|
- **Native icons from your SVGs**: at prebuild, every SVG in the folder becomes an iOS asset-catalog image and an
|
|
26
|
-
Android vector drawable, and `
|
|
26
|
+
Android vector drawable, and `native-icons.d.ts` types their names (`bolt.svg` → `<Icon name="bolt" />`). The
|
|
27
|
+
work is [`@hzblj/expo-native-icons`](https://www.npmjs.com/package/@hzblj/expo-native-icons)'s; `icons` takes its
|
|
28
|
+
options.
|
|
27
29
|
- **iOS semantic colours**: `iosSemanticColors` redefines a UIKit colour such as `labelColor` app-wide.
|
|
28
|
-
- **`kara-icons`**: the same icon check without Expo, for CI; it ships as `@hzblj/kara-ui`'s bin
|
|
30
|
+
- **`kara-icons`**: the same icon check without Expo, for CI; it ships as `@hzblj/kara-ui`'s bin
|
|
31
|
+
(`native-icons` in `@hzblj/expo-native-icons`).
|
|
29
32
|
|
|
30
33
|
## Documentation
|
|
31
34
|
|
package/build/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { type NativeIconsPluginOptions } from '@hzblj/expo-native-icons/plugin';
|
|
1
2
|
import { type ConfigPlugin } from 'expo/config-plugins';
|
|
2
3
|
import { type IosSemanticColorsProps } from './ios-semantic-colors';
|
|
3
|
-
import { type IconsProps } from './with-icons';
|
|
4
|
-
export * from './icons';
|
|
5
4
|
export type { IosColorAppearances, IosSemanticColorsProps } from './ios-semantic-colors';
|
|
6
|
-
export type { IconsProps } from './with-icons';
|
|
7
5
|
export type KaraUIPluginProps = {
|
|
8
|
-
/**
|
|
9
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The app's icons: a folder of SVGs turned into native assets on both platforms, and their names into types.
|
|
8
|
+
* `@hzblj/expo-native-icons` does the work; see its options.
|
|
9
|
+
*/
|
|
10
|
+
icons?: NativeIconsPluginOptions;
|
|
10
11
|
/** Redefines UIKit semantic colours app-wide, natively. See `withIosSemanticColors`. */
|
|
11
12
|
iosSemanticColors?: IosSemanticColorsProps;
|
|
12
13
|
};
|
package/build/index.js
CHANGED
|
@@ -1,32 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const plugin_1 = require("@hzblj/expo-native-icons/plugin");
|
|
17
4
|
const config_plugins_1 = require("expo/config-plugins");
|
|
18
5
|
const ios_semantic_colors_1 = require("./ios-semantic-colors");
|
|
19
|
-
const with_icons_1 = require("./with-icons");
|
|
20
|
-
__exportStar(require("./icons"), exports);
|
|
21
6
|
/**
|
|
22
7
|
* `plugins: [['@hzblj/kara-ui', {icons: {source: './assets/icons'}}]]` in the app config. Everything native the
|
|
23
|
-
* library cannot ship itself: the app's own icons as native assets, and
|
|
24
|
-
* itself resolves to, for every token (and every piece of UIKit) that names it.
|
|
8
|
+
* library cannot ship itself: the app's own icons as native assets (`@hzblj/expo-native-icons`), and optionally what a
|
|
9
|
+
* UIKit semantic colour itself resolves to, for every token (and every piece of UIKit) that names it.
|
|
25
10
|
*/
|
|
26
11
|
const withKaraUI = (config, { icons, iosSemanticColors } = {}) => {
|
|
27
12
|
const withColors = iosSemanticColors ? (0, ios_semantic_colors_1.withIosSemanticColors)(config, iosSemanticColors) : config;
|
|
28
|
-
return icons ? (0,
|
|
13
|
+
return icons ? (0, plugin_1.withNativeIcons)(withColors, icons) : withColors;
|
|
29
14
|
};
|
|
30
|
-
// Read at prebuild from the shipped package.json, beside `build
|
|
15
|
+
// Read at prebuild from the shipped package.json, beside `build/`: outside `rootDir`, so not an import.
|
|
31
16
|
const pkg = require('../package.json');
|
|
32
17
|
exports.default = (0, config_plugins_1.createRunOncePlugin)(withKaraUI, pkg.name, pkg.version);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzblj/kara-expo-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The Expo config plugin and tooling of @hzblj/kara-ui: the app's SVG icons as native assets on iOS and Android, and UIKit semantic colour overrides.",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "The Expo config plugin and tooling of @hzblj/kara-ui: the app's SVG icons as native assets on iOS and Android (through @hzblj/expo-native-icons), and UIKit semantic colour overrides.",
|
|
5
5
|
"author": "Jan Blazej",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -25,10 +25,6 @@
|
|
|
25
25
|
"types": "./build/index.d.ts",
|
|
26
26
|
"default": "./build/index.js"
|
|
27
27
|
},
|
|
28
|
-
"./cli": {
|
|
29
|
-
"types": "./build/cli.d.ts",
|
|
30
|
-
"default": "./build/cli.js"
|
|
31
|
-
},
|
|
32
28
|
"./app.plugin.js": "./app.plugin.js",
|
|
33
29
|
"./package.json": "./package.json"
|
|
34
30
|
},
|
|
@@ -45,6 +41,9 @@
|
|
|
45
41
|
"biome:check": "yarn run -T biome check .",
|
|
46
42
|
"tsc:check": "tsc -p ./tsconfig.json --noEmit"
|
|
47
43
|
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@hzblj/expo-native-icons": "^0.1.0"
|
|
46
|
+
},
|
|
48
47
|
"peerDependencies": {
|
|
49
48
|
"expo": "*"
|
|
50
49
|
},
|
package/build/cli.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `npx kara-icons ./assets/icons` — the half of custom icons that runs before prebuild: validation, so a bad SVG fails
|
|
3
|
-
* here rather than mid-prebuild, and the names, so the app type-checks against the icons it actually ships.
|
|
4
|
-
*/
|
|
5
|
-
export declare const main: (args: string[]) => number;
|
package/build/cli.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.main = void 0;
|
|
7
|
-
const node_fs_1 = require("node:fs");
|
|
8
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const icons_1 = require("./icons");
|
|
10
|
-
const USAGE = `Usage: kara-icons <source> [--out <file>]
|
|
11
|
-
|
|
12
|
-
Checks every SVG in <source> the way prebuild will, and writes the TypeScript declaration that makes
|
|
13
|
-
their names valid for <Icon name="…" />. The config plugin does the same whenever the app config is
|
|
14
|
-
read; this is for CI and for projects that type-check without starting Expo.
|
|
15
|
-
|
|
16
|
-
--out where to write the declaration (default: kara-icons.d.ts in the current directory)`;
|
|
17
|
-
/**
|
|
18
|
-
* `npx kara-icons ./assets/icons` — the half of custom icons that runs before prebuild: validation, so a bad SVG fails
|
|
19
|
-
* here rather than mid-prebuild, and the names, so the app type-checks against the icons it actually ships.
|
|
20
|
-
*/
|
|
21
|
-
const main = (args) => {
|
|
22
|
-
const outIndex = args.indexOf('--out');
|
|
23
|
-
const outValueIndex = outIndex === -1 ? -1 : outIndex + 1;
|
|
24
|
-
const out = outIndex === -1 ? 'kara-icons.d.ts' : args[outValueIndex];
|
|
25
|
-
const source = args.find((arg, index) => !arg.startsWith('--') && index !== outValueIndex);
|
|
26
|
-
if (!source || !out || args.includes('--help')) {
|
|
27
|
-
console.log(USAGE);
|
|
28
|
-
return source ? 0 : 1;
|
|
29
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
const { android, ios, names } = (0, icons_1.readIconFolder)(node_path_1.default.resolve(source));
|
|
32
|
-
(0, node_fs_1.writeFileSync)(node_path_1.default.resolve(out), (0, icons_1.iconTypes)(names, source));
|
|
33
|
-
console.log(`kara-icons: ${names.length} icons (${ios.length} iOS, ${android.length} Android) → ${out}`);
|
|
34
|
-
return 0;
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
console.error(error instanceof Error ? error.message : error);
|
|
38
|
-
return 1;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
exports.main = main;
|
package/build/icons.d.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/** One `<path>` of a glyph. `evenOdd` keeps holes punched by an inner contour, the way Figma exports them. */
|
|
2
|
-
export type IconPath = {
|
|
3
|
-
d: string;
|
|
4
|
-
evenOdd: boolean;
|
|
5
|
-
};
|
|
6
|
-
/** A single-colour glyph, read from an SVG: its viewBox and the paths drawn in it. */
|
|
7
|
-
export type SvgIcon = {
|
|
8
|
-
height: number;
|
|
9
|
-
minX: number;
|
|
10
|
-
minY: number;
|
|
11
|
-
name: string;
|
|
12
|
-
paths: IconPath[];
|
|
13
|
-
width: number;
|
|
14
|
-
};
|
|
15
|
-
export type IconPlatform = 'android' | 'ios';
|
|
16
|
-
/** `arrow-left` → `arrow_left`: Android resource names allow neither `-` nor capitals. */
|
|
17
|
-
export declare const snakeCase: (name: string) => string;
|
|
18
|
-
/** An app icon's native name, the same on both platforms: `kara_icon_<name>` — the asset, and the drawable. */
|
|
19
|
-
export declare const iconResourceName: (name: string) => string;
|
|
20
|
-
/**
|
|
21
|
-
* Reads one glyph. Only `<path>` elements (optionally `fill-rule="evenodd"`) inside one `<svg>` with a numeric
|
|
22
|
-
* `viewBox` are accepted, and anything else fails loudly — a glyph that silently lost a clip or a mask is worse than
|
|
23
|
-
* none (a mask leftover flattens into a solid square). The colour in the file is ignored: every icon is a template the
|
|
24
|
-
* platform tints.
|
|
25
|
-
*/
|
|
26
|
-
export declare const parseSvgIcon: (file: string) => SvgIcon;
|
|
27
|
-
/** What one folder holds: each platform's glyphs, and every name that exists on at least one of them. */
|
|
28
|
-
export type IconFolder = {
|
|
29
|
-
android: SvgIcon[];
|
|
30
|
-
ios: SvgIcon[];
|
|
31
|
-
names: string[];
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Every icon in a folder, per platform: `<name>.svg` for both, `<name>.ios.svg` / `<name>.android.svg` for one — a
|
|
35
|
-
* variant wins over the shared file on its platform, and a name with only a variant exists on that platform alone.
|
|
36
|
-
* Refused when two names would land on one native resource name (`arrow-left` and `arrow_left`).
|
|
37
|
-
*/
|
|
38
|
-
export declare const readIconFolder: (directory: string) => IconFolder;
|
|
39
|
-
/**
|
|
40
|
-
* An asset-catalog image set: the glyph as an SVG template that keeps its vector data, so SwiftUI's `resizable()` and
|
|
41
|
-
* `UIImageView` scale it crisply to any size, and UIKit tints it with the view's tint colour. `size` is the point size
|
|
42
|
-
* a system control draws it at when nothing resizes it.
|
|
43
|
-
*/
|
|
44
|
-
export declare const iosImageset: (icon: SvgIcon, assetName: string, size: number, author: string) => {
|
|
45
|
-
contents: string;
|
|
46
|
-
svg: string;
|
|
47
|
-
};
|
|
48
|
-
/**
|
|
49
|
-
* The glyph as an Android vector drawable — compiled by aapt like any resource, drawn by `ImageView` and Compose's
|
|
50
|
-
* `painterResource` alike. White fill, because every consumer tints it. A viewBox that does not start at the origin
|
|
51
|
-
* (Material Symbols use `0 -960 960 960`) is moved there by a group, since a vector's viewport always starts at 0,0.
|
|
52
|
-
*/
|
|
53
|
-
export declare const androidVectorDrawable: (icon: SvgIcon, size?: number) => string;
|
|
54
|
-
/**
|
|
55
|
-
* The declaration that teaches TypeScript the app's icon names: a global `KaraIcons` the components read, so
|
|
56
|
-
* `<Icon name="bolt" />` type-checks without any import — and an icon deleted from the folder fails at its call sites.
|
|
57
|
-
*/
|
|
58
|
-
export declare const iconTypes: (names: string[], source: string) => string;
|
package/build/icons.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.iconTypes = exports.androidVectorDrawable = exports.iosImageset = exports.readIconFolder = exports.parseSvgIcon = exports.iconResourceName = exports.snakeCase = void 0;
|
|
7
|
-
const node_fs_1 = require("node:fs");
|
|
8
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
/** Kebab or snake case, lower case, starting with a letter: the file name is the icon name. */
|
|
10
|
-
const ICON_NAME = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
11
|
-
/** `share.android.svg` replaces `share.svg` on Android only — the platform's own glyph where the two differ. */
|
|
12
|
-
const PLATFORM_SUFFIX = /\.(android|ios)$/;
|
|
13
|
-
/** Every element the generator understands. Anything else — groups, clips, masks, strokes — must be flattened first. */
|
|
14
|
-
const ALLOWED_TAGS = new Set(['path', 'svg', 'title']);
|
|
15
|
-
const fail = (file, message) => {
|
|
16
|
-
throw new Error(`[kara-icons] ${node_path_1.default.basename(file)}: ${message}`);
|
|
17
|
-
};
|
|
18
|
-
/** `arrow-left` → `arrow_left`: Android resource names allow neither `-` nor capitals. */
|
|
19
|
-
const snakeCase = (name) => name.replace(/-/g, '_').toLowerCase();
|
|
20
|
-
exports.snakeCase = snakeCase;
|
|
21
|
-
/** An app icon's native name, the same on both platforms: `kara_icon_<name>` — the asset, and the drawable. */
|
|
22
|
-
const iconResourceName = (name) => `kara_icon_${(0, exports.snakeCase)(name)}`;
|
|
23
|
-
exports.iconResourceName = iconResourceName;
|
|
24
|
-
/**
|
|
25
|
-
* Reads one glyph. Only `<path>` elements (optionally `fill-rule="evenodd"`) inside one `<svg>` with a numeric
|
|
26
|
-
* `viewBox` are accepted, and anything else fails loudly — a glyph that silently lost a clip or a mask is worse than
|
|
27
|
-
* none (a mask leftover flattens into a solid square). The colour in the file is ignored: every icon is a template the
|
|
28
|
-
* platform tints.
|
|
29
|
-
*/
|
|
30
|
-
const parseSvgIcon = (file) => {
|
|
31
|
-
const name = node_path_1.default.basename(file, '.svg').replace(PLATFORM_SUFFIX, '');
|
|
32
|
-
const svg = (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
33
|
-
if (!ICON_NAME.test(name)) {
|
|
34
|
-
fail(file, 'name it in lower kebab or snake case, starting with a letter — the file name is the icon name');
|
|
35
|
-
}
|
|
36
|
-
const viewBox = svg.match(/viewBox="\s*(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*"/);
|
|
37
|
-
if (!viewBox) {
|
|
38
|
-
fail(file, 'expected a numeric viewBox="<minX> <minY> <width> <height>"');
|
|
39
|
-
}
|
|
40
|
-
for (const [, tag] of svg.matchAll(/<([a-zA-Z][\w-]*)/g)) {
|
|
41
|
-
if (!ALLOWED_TAGS.has(tag)) {
|
|
42
|
-
fail(file, `only <path> elements are supported, found <${tag}> — flatten groups, clips, masks and strokes first`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const paths = [];
|
|
46
|
-
for (const [, attributes] of svg.matchAll(/<path\b([^>]*)>/g)) {
|
|
47
|
-
const d = attributes.match(/\sd="([^"]+)"/)?.[1];
|
|
48
|
-
if (!d) {
|
|
49
|
-
fail(file, '<path> without d');
|
|
50
|
-
}
|
|
51
|
-
paths.push({ d: d, evenOdd: /fill-rule="evenodd"/.test(attributes) });
|
|
52
|
-
}
|
|
53
|
-
if (paths.length === 0) {
|
|
54
|
-
fail(file, 'no <path> elements');
|
|
55
|
-
}
|
|
56
|
-
const [, minX, minY, width, height] = viewBox;
|
|
57
|
-
return { height: Number(height), minX: Number(minX), minY: Number(minY), name, paths, width: Number(width) };
|
|
58
|
-
};
|
|
59
|
-
exports.parseSvgIcon = parseSvgIcon;
|
|
60
|
-
const glyphsFor = (directory, names, platform) => names.flatMap(name => {
|
|
61
|
-
const variant = node_path_1.default.join(directory, `${name}.${platform}.svg`);
|
|
62
|
-
const shared = node_path_1.default.join(directory, `${name}.svg`);
|
|
63
|
-
if ((0, node_fs_1.existsSync)(variant)) {
|
|
64
|
-
return [(0, exports.parseSvgIcon)(variant)];
|
|
65
|
-
}
|
|
66
|
-
return (0, node_fs_1.existsSync)(shared) ? [(0, exports.parseSvgIcon)(shared)] : [];
|
|
67
|
-
});
|
|
68
|
-
/**
|
|
69
|
-
* Every icon in a folder, per platform: `<name>.svg` for both, `<name>.ios.svg` / `<name>.android.svg` for one — a
|
|
70
|
-
* variant wins over the shared file on its platform, and a name with only a variant exists on that platform alone.
|
|
71
|
-
* Refused when two names would land on one native resource name (`arrow-left` and `arrow_left`).
|
|
72
|
-
*/
|
|
73
|
-
const readIconFolder = (directory) => {
|
|
74
|
-
if (!(0, node_fs_1.existsSync)(directory)) {
|
|
75
|
-
throw new Error(`[kara-icons] No icon folder at ${directory}.`);
|
|
76
|
-
}
|
|
77
|
-
const names = [
|
|
78
|
-
...new Set((0, node_fs_1.readdirSync)(directory)
|
|
79
|
-
.filter(file => file.endsWith('.svg'))
|
|
80
|
-
.map(file => node_path_1.default.basename(file, '.svg').replace(PLATFORM_SUFFIX, ''))),
|
|
81
|
-
].sort();
|
|
82
|
-
const byResource = new Map();
|
|
83
|
-
for (const name of names) {
|
|
84
|
-
const resource = (0, exports.snakeCase)(name);
|
|
85
|
-
const existing = byResource.get(resource);
|
|
86
|
-
if (existing) {
|
|
87
|
-
throw new Error(`[kara-icons] "${existing}" and "${name}" would both become the resource "${resource}".`);
|
|
88
|
-
}
|
|
89
|
-
byResource.set(resource, name);
|
|
90
|
-
}
|
|
91
|
-
return { android: glyphsFor(directory, names, 'android'), ios: glyphsFor(directory, names, 'ios'), names };
|
|
92
|
-
};
|
|
93
|
-
exports.readIconFolder = readIconFolder;
|
|
94
|
-
const fillRule = (iconPath) => (iconPath.evenOdd ? ' fill-rule="evenodd"' : '');
|
|
95
|
-
/**
|
|
96
|
-
* An asset-catalog image set: the glyph as an SVG template that keeps its vector data, so SwiftUI's `resizable()` and
|
|
97
|
-
* `UIImageView` scale it crisply to any size, and UIKit tints it with the view's tint colour. `size` is the point size
|
|
98
|
-
* a system control draws it at when nothing resizes it.
|
|
99
|
-
*/
|
|
100
|
-
const iosImageset = (icon, assetName, size, author) => {
|
|
101
|
-
const paths = icon.paths.map(iconPath => ` <path d="${iconPath.d}"${fillRule(iconPath)} fill="black"/>`);
|
|
102
|
-
const viewBox = `${icon.minX} ${icon.minY} ${icon.width} ${icon.height}`;
|
|
103
|
-
return {
|
|
104
|
-
contents: `${JSON.stringify({
|
|
105
|
-
images: [{ filename: `${assetName}.svg`, idiom: 'universal' }],
|
|
106
|
-
info: { author, version: 1 },
|
|
107
|
-
properties: { 'preserves-vector-representation': true, 'template-rendering-intent': 'template' },
|
|
108
|
-
}, null, 2)}\n`,
|
|
109
|
-
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="${viewBox}">\n${paths.join('\n')}\n</svg>\n`,
|
|
110
|
-
};
|
|
111
|
-
};
|
|
112
|
-
exports.iosImageset = iosImageset;
|
|
113
|
-
/**
|
|
114
|
-
* The glyph as an Android vector drawable — compiled by aapt like any resource, drawn by `ImageView` and Compose's
|
|
115
|
-
* `painterResource` alike. White fill, because every consumer tints it. A viewBox that does not start at the origin
|
|
116
|
-
* (Material Symbols use `0 -960 960 960`) is moved there by a group, since a vector's viewport always starts at 0,0.
|
|
117
|
-
*/
|
|
118
|
-
const androidVectorDrawable = (icon, size = 24) => {
|
|
119
|
-
const paths = icon.paths.map(iconPath => ` <path\n android:fillColor="@android:color/white"${iconPath.evenOdd ? '\n android:fillType="evenOdd"' : ''}\n android:pathData="${iconPath.d}"/>`);
|
|
120
|
-
const offset = icon.minX !== 0 || icon.minY !== 0;
|
|
121
|
-
const body = offset
|
|
122
|
-
? ` <group android:translateX="${-icon.minX}" android:translateY="${-icon.minY}">\n${paths.join('\n')}\n </group>`
|
|
123
|
-
: paths.join('\n').replace(/^ {2}/gm, '');
|
|
124
|
-
return `<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
125
|
-
android:width="${size}dp"
|
|
126
|
-
android:height="${size}dp"
|
|
127
|
-
android:viewportWidth="${icon.width}"
|
|
128
|
-
android:viewportHeight="${icon.height}">
|
|
129
|
-
${body}
|
|
130
|
-
</vector>
|
|
131
|
-
`;
|
|
132
|
-
};
|
|
133
|
-
exports.androidVectorDrawable = androidVectorDrawable;
|
|
134
|
-
/**
|
|
135
|
-
* The declaration that teaches TypeScript the app's icon names: a global `KaraIcons` the components read, so
|
|
136
|
-
* `<Icon name="bolt" />` type-checks without any import — and an icon deleted from the folder fails at its call sites.
|
|
137
|
-
*/
|
|
138
|
-
const iconTypes = (names, source) => {
|
|
139
|
-
const key = (name) => (/^[A-Za-z_$][\w$]*$/.test(name) ? name : `'${name}'`);
|
|
140
|
-
const entries = names.map(name => ` ${key(name)}: true`);
|
|
141
|
-
return `// Generated by @hzblj/kara-ui from ${source} — do not edit. It is rewritten whenever the app config is read
|
|
142
|
-
// (\`expo start\`, \`expo prebuild\`), or by \`npx kara-icons ${source}\`.
|
|
143
|
-
declare global {
|
|
144
|
-
interface KaraIcons {
|
|
145
|
-
${entries.join('\n')}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export {}
|
|
150
|
-
`;
|
|
151
|
-
};
|
|
152
|
-
exports.iconTypes = iconTypes;
|
package/build/with-icons.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { type ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
-
export type IconsProps = {
|
|
3
|
-
/**
|
|
4
|
-
* A folder of SVGs, one glyph per file, relative to the project root. The file name is the icon name
|
|
5
|
-
* (`bolt.svg` → `<Icon name="bolt" />`); `share.android.svg` replaces `share.svg` on Android. Only single-colour
|
|
6
|
-
* `<path>`s are accepted — the platform tints every icon.
|
|
7
|
-
*/
|
|
8
|
-
source: string;
|
|
9
|
-
/**
|
|
10
|
-
* Where the declaration of the icon names goes, relative to the project root — `false` to skip it. Rewritten
|
|
11
|
-
* whenever the app config is read, so `expo start` alone keeps it current. `kara-icons.d.ts` by default.
|
|
12
|
-
*/
|
|
13
|
-
types?: string | false;
|
|
14
|
-
/**
|
|
15
|
-
* The point size iOS system controls (bar items, menus, swipe actions) draw an icon at when nothing resizes it.
|
|
16
|
-
* Everything kara-ui renders sizes the icon itself. 24 by default.
|
|
17
|
-
*/
|
|
18
|
-
iosSize?: number;
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* The app's icons as native assets on both platforms, generated from SVGs at prebuild: a template image set per icon in
|
|
22
|
-
* the iOS asset catalog (vector data kept), a vector drawable per icon in Android's `res/drawable`. They have to live
|
|
23
|
-
* in the app's own catalog and resources — `Image("…")` and `UIImage(named:)` look in the main bundle, and an asset
|
|
24
|
-
* catalog shipped by a pod collides with the app's `Assets.car`. A new or changed SVG needs a prebuild.
|
|
25
|
-
*/
|
|
26
|
-
export declare const withIcons: ConfigPlugin<IconsProps>;
|
package/build/with-icons.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.withIcons = void 0;
|
|
7
|
-
const node_fs_1 = require("node:fs");
|
|
8
|
-
const promises_1 = require("node:fs/promises");
|
|
9
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
-
const config_plugins_1 = require("expo/config-plugins");
|
|
11
|
-
const icons_1 = require("./icons");
|
|
12
|
-
/** Stamped into every image set this writes, so a later run can tell ours from one added by hand. */
|
|
13
|
-
const AUTHOR = '@hzblj/kara-ui';
|
|
14
|
-
const IMAGESET_SUFFIX = '.imageset';
|
|
15
|
-
const RESOURCE_PREFIX = (0, icons_1.iconResourceName)('');
|
|
16
|
-
const DEFAULT_IOS_SIZE = 24;
|
|
17
|
-
const DEFAULT_TYPES = 'kara-icons.d.ts';
|
|
18
|
-
/** Removes what an earlier run wrote for icons no longer in the folder — a deleted SVG must stop existing natively. */
|
|
19
|
-
const removeStaleImagesets = async (catalog, keep) => {
|
|
20
|
-
const dirents = await (0, promises_1.readdir)(catalog, { withFileTypes: true }).catch(() => []);
|
|
21
|
-
for (const dirent of dirents) {
|
|
22
|
-
const name = dirent.name.slice(0, -IMAGESET_SUFFIX.length);
|
|
23
|
-
if (!dirent.isDirectory() || !dirent.name.endsWith(IMAGESET_SUFFIX) || keep.has(name)) {
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
const setPath = node_path_1.default.join(catalog, dirent.name);
|
|
27
|
-
const contents = await (0, promises_1.readFile)(node_path_1.default.join(setPath, 'Contents.json'), 'utf8').catch(() => '');
|
|
28
|
-
if (name.startsWith(RESOURCE_PREFIX) && contents.includes(`"author": "${AUTHOR}"`)) {
|
|
29
|
-
await (0, promises_1.rm)(setPath, { force: true, recursive: true });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
const withIosIcons = (config, { iosSize = DEFAULT_IOS_SIZE, source }) => (0, config_plugins_1.withDangerousMod)(config, [
|
|
34
|
-
'ios',
|
|
35
|
-
async (config) => {
|
|
36
|
-
const { projectRoot } = config.modRequest;
|
|
37
|
-
const { ios } = (0, icons_1.readIconFolder)(node_path_1.default.resolve(projectRoot, source));
|
|
38
|
-
const catalog = node_path_1.default.join(config_plugins_1.IOSConfig.Paths.getSourceRoot(projectRoot), 'Images.xcassets');
|
|
39
|
-
const written = new Set();
|
|
40
|
-
for (const icon of ios) {
|
|
41
|
-
const assetName = (0, icons_1.iconResourceName)(icon.name);
|
|
42
|
-
const setPath = node_path_1.default.join(catalog, `${assetName}${IMAGESET_SUFFIX}`);
|
|
43
|
-
const { contents, svg } = (0, icons_1.iosImageset)(icon, assetName, iosSize, AUTHOR);
|
|
44
|
-
written.add(assetName);
|
|
45
|
-
await (0, promises_1.mkdir)(setPath, { recursive: true });
|
|
46
|
-
await (0, promises_1.writeFile)(node_path_1.default.join(setPath, 'Contents.json'), contents, 'utf8');
|
|
47
|
-
await (0, promises_1.writeFile)(node_path_1.default.join(setPath, `${assetName}.svg`), svg, 'utf8');
|
|
48
|
-
}
|
|
49
|
-
await removeStaleImagesets(catalog, written);
|
|
50
|
-
return config;
|
|
51
|
-
},
|
|
52
|
-
]);
|
|
53
|
-
const withAndroidIcons = (config, { source }) => (0, config_plugins_1.withDangerousMod)(config, [
|
|
54
|
-
'android',
|
|
55
|
-
async (config) => {
|
|
56
|
-
const { platformProjectRoot, projectRoot } = config.modRequest;
|
|
57
|
-
const { android } = (0, icons_1.readIconFolder)(node_path_1.default.resolve(projectRoot, source));
|
|
58
|
-
const drawables = node_path_1.default.join(platformProjectRoot, 'app', 'src', 'main', 'res', 'drawable');
|
|
59
|
-
const written = new Set(android.map(icon => `${(0, icons_1.iconResourceName)(icon.name)}.xml`));
|
|
60
|
-
await (0, promises_1.mkdir)(drawables, { recursive: true });
|
|
61
|
-
for (const icon of android) {
|
|
62
|
-
await (0, promises_1.writeFile)(node_path_1.default.join(drawables, `${(0, icons_1.iconResourceName)(icon.name)}.xml`), (0, icons_1.androidVectorDrawable)(icon), 'utf8');
|
|
63
|
-
}
|
|
64
|
-
// The `kara_icon_` prefix is this plugin's alone, so any such drawable not written now was written before.
|
|
65
|
-
for (const file of await (0, promises_1.readdir)(drawables)) {
|
|
66
|
-
if (file.startsWith(RESOURCE_PREFIX) && file.endsWith('.xml') && !written.has(file)) {
|
|
67
|
-
await (0, promises_1.rm)(node_path_1.default.join(drawables, file), { force: true });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return config;
|
|
71
|
-
},
|
|
72
|
-
]);
|
|
73
|
-
/**
|
|
74
|
-
* Keeps the declaration of icon names in step with the folder. Done while the config is evaluated rather than in a mod
|
|
75
|
-
* on purpose: mods run only at prebuild, while `expo start` reads the config too — so adding an SVG and starting Metro
|
|
76
|
-
* is all it takes for its name to type-check. Written only when the content changed.
|
|
77
|
-
*/
|
|
78
|
-
const writeTypes = (projectRoot, { source, types = DEFAULT_TYPES }) => {
|
|
79
|
-
if (types === false) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const { names } = (0, icons_1.readIconFolder)(node_path_1.default.resolve(projectRoot, source));
|
|
83
|
-
const file = node_path_1.default.resolve(projectRoot, types);
|
|
84
|
-
const contents = (0, icons_1.iconTypes)(names, source);
|
|
85
|
-
if (!(0, node_fs_1.existsSync)(file) || (0, node_fs_1.readFileSync)(file, 'utf8') !== contents) {
|
|
86
|
-
(0, node_fs_1.writeFileSync)(file, contents);
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
/**
|
|
90
|
-
* The app's icons as native assets on both platforms, generated from SVGs at prebuild: a template image set per icon in
|
|
91
|
-
* the iOS asset catalog (vector data kept), a vector drawable per icon in Android's `res/drawable`. They have to live
|
|
92
|
-
* in the app's own catalog and resources — `Image("…")` and `UIImage(named:)` look in the main bundle, and an asset
|
|
93
|
-
* catalog shipped by a pod collides with the app's `Assets.car`. A new or changed SVG needs a prebuild.
|
|
94
|
-
*/
|
|
95
|
-
const withIcons = (config, props) => {
|
|
96
|
-
writeTypes(config._internal?.projectRoot ?? process.cwd(), props);
|
|
97
|
-
return withAndroidIcons(withIosIcons(config, props), props);
|
|
98
|
-
};
|
|
99
|
-
exports.withIcons = withIcons;
|