@hzblj/kara-expo-plugin 0.0.0-stage → 0.2.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/README.md +7 -2
- package/app.plugin.js +2 -0
- package/build/cli.d.ts +5 -0
- package/build/cli.js +40 -0
- package/build/icons.d.ts +58 -0
- package/build/icons.js +152 -0
- package/build/index.d.ts +14 -0
- package/build/index.js +32 -0
- package/build/ios-semantic-colors.d.ts +49 -0
- package/build/ios-semantic-colors.js +193 -0
- package/build/with-icons.d.ts +26 -0
- package/build/with-icons.js +99 -0
- package/package.json +57 -4
package/README.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hzblj/kara-expo-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The optional Expo config plugin of kara-ui. It redefines UIKit semantic colours app-wide, natively, from a JSON file or inline values — `plugins: [['@hzblj/kara-ui', {iosSemanticColors: {file: './ios-colors.json'}}]]`.
|
|
4
|
+
|
|
5
|
+
**Install [`@hzblj/kara-ui`](https://www.npmjs.com/package/@hzblj/kara-ui) instead.** This package is one of its building
|
|
6
|
+
blocks, published so the umbrella can depend on it; it is versioned together with it and has no standalone docs.
|
|
7
|
+
|
|
8
|
+
MIT
|
package/app.plugin.js
ADDED
package/build/cli.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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 out = outIndex === -1 ? 'kara-icons.d.ts' : args[outIndex + 1];
|
|
24
|
+
const source = args.find((arg, index) => !arg.startsWith('--') && index !== outIndex + 1);
|
|
25
|
+
if (!source || !out || args.includes('--help')) {
|
|
26
|
+
console.log(USAGE);
|
|
27
|
+
return source ? 0 : 1;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const { android, ios, names } = (0, icons_1.readIconFolder)(node_path_1.default.resolve(source));
|
|
31
|
+
(0, node_fs_1.writeFileSync)(node_path_1.default.resolve(out), (0, icons_1.iconTypes)(names, source));
|
|
32
|
+
console.log(`kara-icons: ${names.length} icons (${ios.length} iOS, ${android.length} Android) → ${out}`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error(error instanceof Error ? error.message : error);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
exports.main = main;
|
package/build/icons.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
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/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
+
import { type IosSemanticColorsProps } from './ios-semantic-colors';
|
|
3
|
+
import { type IconsProps } from './with-icons';
|
|
4
|
+
export * from './icons';
|
|
5
|
+
export type { IosColorAppearances, IosSemanticColorsProps } from './ios-semantic-colors';
|
|
6
|
+
export type { IconsProps } from './with-icons';
|
|
7
|
+
export type KaraUIPluginProps = {
|
|
8
|
+
/** The app's icons: a folder of SVGs turned into native assets on both platforms, and their names into types. */
|
|
9
|
+
icons?: IconsProps;
|
|
10
|
+
/** Redefines UIKit semantic colours app-wide, natively. See `withIosSemanticColors`. */
|
|
11
|
+
iosSemanticColors?: IosSemanticColorsProps;
|
|
12
|
+
};
|
|
13
|
+
declare const _default: ConfigPlugin<KaraUIPluginProps | undefined>;
|
|
14
|
+
export default _default;
|
package/build/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
const config_plugins_1 = require("expo/config-plugins");
|
|
18
|
+
const ios_semantic_colors_1 = require("./ios-semantic-colors");
|
|
19
|
+
const with_icons_1 = require("./with-icons");
|
|
20
|
+
__exportStar(require("./icons"), exports);
|
|
21
|
+
/**
|
|
22
|
+
* `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 — optionally — what a UIKit semantic colour
|
|
24
|
+
* itself resolves to, for every token (and every piece of UIKit) that names it.
|
|
25
|
+
*/
|
|
26
|
+
const withKaraUI = (config, { icons, iosSemanticColors } = {}) => {
|
|
27
|
+
const withColors = iosSemanticColors ? (0, ios_semantic_colors_1.withIosSemanticColors)(config, iosSemanticColors) : config;
|
|
28
|
+
return icons ? (0, with_icons_1.withIcons)(withColors, icons) : withColors;
|
|
29
|
+
};
|
|
30
|
+
// Read at prebuild from the shipped package.json, beside `build/` — outside `rootDir`, so not an import.
|
|
31
|
+
const pkg = require('../package.json');
|
|
32
|
+
exports.default = (0, config_plugins_1.createRunOncePlugin)(withKaraUI, pkg.name, pkg.version);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
+
/**
|
|
3
|
+
* Our own values for one UIKit semantic colour. `light` and `dark` are required — a colour set without both
|
|
4
|
+
* would leave one appearance to fall through to Apple's value, which is the confusing half-override.
|
|
5
|
+
*/
|
|
6
|
+
export type IosColorAppearances = {
|
|
7
|
+
dark: string;
|
|
8
|
+
/** Used instead of `dark` when the user has asked for increased contrast. */
|
|
9
|
+
highContrastDark?: string;
|
|
10
|
+
/** Used instead of `light` when the user has asked for increased contrast. */
|
|
11
|
+
highContrastLight?: string;
|
|
12
|
+
light: string;
|
|
13
|
+
};
|
|
14
|
+
export type IosSemanticColorsProps = {
|
|
15
|
+
/**
|
|
16
|
+
* The overrides, keyed by UIKit semantic name. Wins over anything in `file` for the same key, so a designer
|
|
17
|
+
* can hand over a JSON and the config can still pin one colour.
|
|
18
|
+
*/
|
|
19
|
+
colors?: Record<string, IosColorAppearances>;
|
|
20
|
+
/** Path to a JSON file of the same shape, relative to the project root. */
|
|
21
|
+
file?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Builds one colour set's `Contents.json`. Exported so the mapping can be exercised without a prebuild.
|
|
25
|
+
*
|
|
26
|
+
* A colour set carries one entry per appearance combination it answers. The plain entry is the fallback UIKit uses
|
|
27
|
+
* when nothing more specific matches, so light lives there rather than under `luminosity: light`.
|
|
28
|
+
*/
|
|
29
|
+
export declare const iosColorSet: (name: string, appearances: IosColorAppearances) => string;
|
|
30
|
+
export declare const assertOverridable: (names: string[]) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Redefines what a UIKit semantic colour resolves to, app-wide, by writing a colour set of that exact name into
|
|
33
|
+
* the generated asset catalog. Both bridges look a semantic name up with `UIColor(named:)` *before* falling back
|
|
34
|
+
* to the platform's own colour, so an asset of that name wins — which is what makes this an override rather than
|
|
35
|
+
* one more token.
|
|
36
|
+
*
|
|
37
|
+
* Two things it does not do, and both matter:
|
|
38
|
+
*
|
|
39
|
+
* - It reaches only code that resolves colours *by name*, which means our own tokens. UIKit asking itself for
|
|
40
|
+
* `UIColor.label` is a class property, not a lookup, so system chrome is unlikely to follow. Verify on device
|
|
41
|
+
* before relying on it.
|
|
42
|
+
* - It has no Android counterpart. Material roles are computed from the wallpaper, `materialSeed` or the M3
|
|
43
|
+
* baseline, none of which an app can reach into — so anything overridden here needs `adaptive(...)` in the
|
|
44
|
+
* palette to match on the other platform.
|
|
45
|
+
*
|
|
46
|
+
* Prefer `adaptive({light, dark})` in the palette for a single colour. This earns its keep when one semantic
|
|
47
|
+
* backs many tokens and you want all of them moved by one edit.
|
|
48
|
+
*/
|
|
49
|
+
export declare const withIosSemanticColors: ConfigPlugin<IosSemanticColorsProps>;
|
|
@@ -0,0 +1,193 @@
|
|
|
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.withIosSemanticColors = exports.assertOverridable = exports.iosColorSet = void 0;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const config_plugins_1 = require("expo/config-plugins");
|
|
10
|
+
/**
|
|
11
|
+
* The names this plugin will write a colour set for. A value rather than a type because JSON is not type-checked — a
|
|
12
|
+
* typo would otherwise produce an asset nobody asks for, and the override would silently not apply.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately a copy of the theme's `IosSemanticColor` rather than an import of it: the plugin is compiled for Node on
|
|
15
|
+
* its own, and the theme sits behind platform-split files that only Metro resolves. A name added there and not here
|
|
16
|
+
* fails loudly at prebuild, listing what is known, rather than silently doing nothing.
|
|
17
|
+
*/
|
|
18
|
+
const OVERRIDABLE = [
|
|
19
|
+
'blueColor',
|
|
20
|
+
'clearColor',
|
|
21
|
+
'darkTextColor',
|
|
22
|
+
'labelColor',
|
|
23
|
+
'lightTextColor',
|
|
24
|
+
'linkColor',
|
|
25
|
+
'opaqueSeparatorColor',
|
|
26
|
+
'placeholderTextColor',
|
|
27
|
+
'quaternaryLabelColor',
|
|
28
|
+
'quaternarySystemFillColor',
|
|
29
|
+
'secondaryLabelColor',
|
|
30
|
+
'secondarySystemBackgroundColor',
|
|
31
|
+
'secondarySystemFillColor',
|
|
32
|
+
'secondarySystemGroupedBackgroundColor',
|
|
33
|
+
'separatorColor',
|
|
34
|
+
'systemBackgroundColor',
|
|
35
|
+
'systemBlueColor',
|
|
36
|
+
'systemBrownColor',
|
|
37
|
+
'systemCyanColor',
|
|
38
|
+
'systemFillColor',
|
|
39
|
+
'systemGray2Color',
|
|
40
|
+
'systemGray3Color',
|
|
41
|
+
'systemGray4Color',
|
|
42
|
+
'systemGray5Color',
|
|
43
|
+
'systemGray6Color',
|
|
44
|
+
'systemGrayColor',
|
|
45
|
+
'systemGreenColor',
|
|
46
|
+
'systemGroupedBackgroundColor',
|
|
47
|
+
'systemIndigoColor',
|
|
48
|
+
'systemMintColor',
|
|
49
|
+
'systemOrangeColor',
|
|
50
|
+
'systemPinkColor',
|
|
51
|
+
'systemPurpleColor',
|
|
52
|
+
'systemRedColor',
|
|
53
|
+
'systemTealColor',
|
|
54
|
+
'systemYellowColor',
|
|
55
|
+
'tertiaryLabelColor',
|
|
56
|
+
'tertiarySystemBackgroundColor',
|
|
57
|
+
'tertiarySystemFillColor',
|
|
58
|
+
'tertiarySystemGroupedBackgroundColor',
|
|
59
|
+
];
|
|
60
|
+
/** Stamped into every set we write, so a later run can tell ours from one a designer added by hand. */
|
|
61
|
+
const AUTHOR = '@hzblj/kara-ui';
|
|
62
|
+
const COLORSET_SUFFIX = '.colorset';
|
|
63
|
+
const HEX_SHORT = 4;
|
|
64
|
+
const HEX_RGB = 7;
|
|
65
|
+
const HEX_RGBA = 9;
|
|
66
|
+
const ALPHA_MAX = 255;
|
|
67
|
+
const byte = (hex) => `0x${hex.toUpperCase()}`;
|
|
68
|
+
/**
|
|
69
|
+
* Accepts `#rgb`, `#rrggbb` and `#rrggbbaa` — the forms the palette itself is written in, alpha last, CSS order.
|
|
70
|
+
* The asset catalog wants the channels as `0xNN` and the alpha as a decimal fraction, hence the split.
|
|
71
|
+
*/
|
|
72
|
+
const components = (name, appearance, hex) => {
|
|
73
|
+
const value = hex.trim();
|
|
74
|
+
if (!/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value)) {
|
|
75
|
+
throw new Error(`[ios-semantic-colors] "${name}.${appearance}" is "${hex}" — expected #rgb, #rrggbb or #rrggbbaa.`);
|
|
76
|
+
}
|
|
77
|
+
const digits = value.length === HEX_SHORT ? [...value.slice(1)].map(digit => `${digit}${digit}`).join('') : value.slice(1, HEX_RGB);
|
|
78
|
+
const alpha = value.length === HEX_RGBA ? Number.parseInt(value.slice(HEX_RGB), 16) / ALPHA_MAX : 1;
|
|
79
|
+
return {
|
|
80
|
+
alpha: alpha.toFixed(3),
|
|
81
|
+
blue: byte(digits.slice(4, 6)),
|
|
82
|
+
green: byte(digits.slice(2, 4)),
|
|
83
|
+
red: byte(digits.slice(0, 2)),
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const entry = (appearances, components) => ({
|
|
87
|
+
...(appearances.length > 0 ? { appearances } : {}),
|
|
88
|
+
color: { 'color-space': 'srgb', components },
|
|
89
|
+
idiom: 'universal',
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* Builds one colour set's `Contents.json`. Exported so the mapping can be exercised without a prebuild.
|
|
93
|
+
*
|
|
94
|
+
* A colour set carries one entry per appearance combination it answers. The plain entry is the fallback UIKit uses
|
|
95
|
+
* when nothing more specific matches, so light lives there rather than under `luminosity: light`.
|
|
96
|
+
*/
|
|
97
|
+
const iosColorSet = (name, appearances) => {
|
|
98
|
+
const colors = [
|
|
99
|
+
entry([], components(name, 'light', appearances.light)),
|
|
100
|
+
entry([{ appearance: 'luminosity', value: 'dark' }], components(name, 'dark', appearances.dark)),
|
|
101
|
+
];
|
|
102
|
+
if (appearances.highContrastLight) {
|
|
103
|
+
colors.push(entry([{ appearance: 'contrast', value: 'high' }], components(name, 'highContrastLight', appearances.highContrastLight)));
|
|
104
|
+
}
|
|
105
|
+
if (appearances.highContrastDark) {
|
|
106
|
+
colors.push(entry([
|
|
107
|
+
{ appearance: 'contrast', value: 'high' },
|
|
108
|
+
{ appearance: 'luminosity', value: 'dark' },
|
|
109
|
+
], components(name, 'highContrastDark', appearances.highContrastDark)));
|
|
110
|
+
}
|
|
111
|
+
return `${JSON.stringify({ colors, info: { author: AUTHOR, version: 1 } }, null, 2)}\n`;
|
|
112
|
+
};
|
|
113
|
+
exports.iosColorSet = iosColorSet;
|
|
114
|
+
const readJsonColors = async (projectRoot, file) => {
|
|
115
|
+
const filePath = node_path_1.default.resolve(projectRoot, file);
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(await (0, promises_1.readFile)(filePath, 'utf8'));
|
|
118
|
+
}
|
|
119
|
+
catch (cause) {
|
|
120
|
+
throw new Error(`[ios-semantic-colors] Could not read "${filePath}".`, { cause });
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const assertOverridable = (names) => {
|
|
124
|
+
const unknown = names.filter(name => !OVERRIDABLE.some(allowed => allowed === name));
|
|
125
|
+
if (unknown.length > 0) {
|
|
126
|
+
throw new Error(`[ios-semantic-colors] Not UIKit semantic colours: ${unknown.join(', ')}. ` +
|
|
127
|
+
`A name must carry its Objective-C \`Color\` suffix — \`labelColor\`, not \`label\`. ` +
|
|
128
|
+
`Known: ${OVERRIDABLE.join(', ')}.`);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
exports.assertOverridable = assertOverridable;
|
|
132
|
+
/**
|
|
133
|
+
* Removes sets this plugin wrote on an earlier run that are no longer configured. Only ours are touched — the
|
|
134
|
+
* author stamp is what keeps a hand-authored set safe — and a stale one matters because a colour left in the
|
|
135
|
+
* catalog keeps overriding long after it was deleted from the config.
|
|
136
|
+
*/
|
|
137
|
+
const removeOrphans = async (catalog, keep) => {
|
|
138
|
+
const dirents = await (0, promises_1.readdir)(catalog, { withFileTypes: true }).catch(() => []);
|
|
139
|
+
for (const dirent of dirents) {
|
|
140
|
+
if (!dirent.isDirectory() || !dirent.name.endsWith(COLORSET_SUFFIX)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const name = dirent.name.slice(0, -COLORSET_SUFFIX.length);
|
|
144
|
+
if (keep.has(name)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const setPath = node_path_1.default.join(catalog, dirent.name);
|
|
148
|
+
const existing = await (0, promises_1.readFile)(node_path_1.default.join(setPath, 'Contents.json'), 'utf8').catch(() => '');
|
|
149
|
+
if (existing.includes(`"author": "${AUTHOR}"`)) {
|
|
150
|
+
await (0, promises_1.rm)(setPath, { force: true, recursive: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Redefines what a UIKit semantic colour resolves to, app-wide, by writing a colour set of that exact name into
|
|
156
|
+
* the generated asset catalog. Both bridges look a semantic name up with `UIColor(named:)` *before* falling back
|
|
157
|
+
* to the platform's own colour, so an asset of that name wins — which is what makes this an override rather than
|
|
158
|
+
* one more token.
|
|
159
|
+
*
|
|
160
|
+
* Two things it does not do, and both matter:
|
|
161
|
+
*
|
|
162
|
+
* - It reaches only code that resolves colours *by name*, which means our own tokens. UIKit asking itself for
|
|
163
|
+
* `UIColor.label` is a class property, not a lookup, so system chrome is unlikely to follow. Verify on device
|
|
164
|
+
* before relying on it.
|
|
165
|
+
* - It has no Android counterpart. Material roles are computed from the wallpaper, `materialSeed` or the M3
|
|
166
|
+
* baseline, none of which an app can reach into — so anything overridden here needs `adaptive(...)` in the
|
|
167
|
+
* palette to match on the other platform.
|
|
168
|
+
*
|
|
169
|
+
* Prefer `adaptive({light, dark})` in the palette for a single colour. This earns its keep when one semantic
|
|
170
|
+
* backs many tokens and you want all of them moved by one edit.
|
|
171
|
+
*/
|
|
172
|
+
const withIosSemanticColors = (config, { colors = {}, file } = {}) => (0, config_plugins_1.withDangerousMod)(config, [
|
|
173
|
+
'ios',
|
|
174
|
+
async (config) => {
|
|
175
|
+
const fromFile = file ? await readJsonColors(config.modRequest.projectRoot, file) : {};
|
|
176
|
+
const merged = { ...fromFile, ...colors };
|
|
177
|
+
const names = Object.keys(merged);
|
|
178
|
+
(0, exports.assertOverridable)(names);
|
|
179
|
+
// Everything is generated — and therefore validated — before anything is written, so a bad hex late in the
|
|
180
|
+
// map cannot leave the catalog holding half an override.
|
|
181
|
+
const sets = names.map(name => [name, (0, exports.iosColorSet)(name, merged[name])]);
|
|
182
|
+
const catalog = node_path_1.default.join(config_plugins_1.IOSConfig.Paths.getSourceRoot(config.modRequest.projectRoot), 'Images.xcassets');
|
|
183
|
+
for (const [name, contents] of sets) {
|
|
184
|
+
const setPath = node_path_1.default.join(catalog, `${name}${COLORSET_SUFFIX}`);
|
|
185
|
+
await (0, promises_1.mkdir)(setPath, { recursive: true });
|
|
186
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(setPath, 'Contents.json'), contents, 'utf8');
|
|
187
|
+
}
|
|
188
|
+
// Sets from an earlier run that are no longer configured, so a deleted override stops overriding.
|
|
189
|
+
await removeOrphans(catalog, new Set(names));
|
|
190
|
+
return config;
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
exports.withIosSemanticColors = withIosSemanticColors;
|
|
@@ -0,0 +1,26 @@
|
|
|
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>;
|
|
@@ -0,0 +1,99 @@
|
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzblj/kara-expo-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.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.",
|
|
5
|
+
"author": "Jan Blazej",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/hzblj/kara-ui.git",
|
|
10
|
+
"directory": "packages/expo-plugin"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/hzblj/kara-ui/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/hzblj/kara-ui/tree/main/packages/expo-plugin#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"expo",
|
|
18
|
+
"config-plugin",
|
|
19
|
+
"kara-ui"
|
|
20
|
+
],
|
|
21
|
+
"main": "./build/index.js",
|
|
22
|
+
"types": "./build/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./build/index.d.ts",
|
|
26
|
+
"default": "./build/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./cli": {
|
|
29
|
+
"types": "./build/cli.d.ts",
|
|
30
|
+
"default": "./build/cli.js"
|
|
31
|
+
},
|
|
32
|
+
"./app.plugin.js": "./app.plugin.js",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"build",
|
|
37
|
+
"app.plugin.js"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p ./tsconfig.json",
|
|
41
|
+
"clean": "rm -rf build",
|
|
42
|
+
"prepack": "yarn run -T turbo run build --filter=$npm_package_name...",
|
|
43
|
+
"lint": "yarn run -T concurrently \"yarn biome:check\" \"yarn tsc:check\"",
|
|
44
|
+
"lint:fix": "yarn run -T biome check --write .",
|
|
45
|
+
"biome:check": "yarn run -T biome check .",
|
|
46
|
+
"tsc:check": "tsc -p ./tsconfig.json --noEmit"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"expo": "*"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.0.0",
|
|
53
|
+
"expo": "~57.0.22",
|
|
54
|
+
"typescript": "~7.0.2"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|