@hzblj/kara-expo-plugin 0.0.0-stage → 0.1.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 +7 -0
- package/README.md +7 -2
- package/app.plugin.js +2 -0
- package/build/index.d.ts +9 -0
- package/build/index.js +13 -0
- package/build/ios-semantic-colors.d.ts +49 -0
- package/build/ios-semantic-colors.js +193 -0
- package/package.json +52 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @hzblj/kara-expo-plugin
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`f919c7b`](https://github.com/hzblj/kara-ui/commit/f919c7b06f3798cebd58eef1581076bf1ee61cd1) Thanks [@hzblj](https://github.com/hzblj)! - First release of the kara-ui design system: theme and tokens, the `sx` engine, atoms, molecules, organisms and `Screen`, native tabs (`@hzblj/kara-ui/router`), the KaraUI native module (Liquid Glass, scroll edge effect, native countdown, context menu with preview, rich tooltip, FAB menu), and `KaraProvider` with theme overrides — colours, the app's own fonts, the Material seed, and Material You dynamic colour on Android. Text uses the platform's face by default. Install `@hzblj/kara-ui`; the scoped packages are its building blocks.
|
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/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
+
import { type IosSemanticColorsProps } from './ios-semantic-colors';
|
|
3
|
+
export type { IosColorAppearances, IosSemanticColorsProps } from './ios-semantic-colors';
|
|
4
|
+
export type KaraUIPluginProps = {
|
|
5
|
+
/** Redefines UIKit semantic colours app-wide, natively. See `withIosSemanticColors`. */
|
|
6
|
+
iosSemanticColors?: IosSemanticColorsProps;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: ConfigPlugin<KaraUIPluginProps | undefined>;
|
|
9
|
+
export default _default;
|
package/build/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_plugins_1 = require("expo/config-plugins");
|
|
4
|
+
const ios_semantic_colors_1 = require("./ios-semantic-colors");
|
|
5
|
+
/**
|
|
6
|
+
* `plugins: [['@hzblj/kara-ui', {iosSemanticColors: {file: './ios-colors.json'}}]]` in the app config. Optional — the
|
|
7
|
+
* library needs no native configuration to run. It exists for the one override the theme cannot make from JS: what a
|
|
8
|
+
* UIKit semantic colour itself resolves to, for every token (and every piece of UIKit) that names it.
|
|
9
|
+
*/
|
|
10
|
+
const withKaraUI = (config, { iosSemanticColors } = {}) => iosSemanticColors ? (0, ios_semantic_colors_1.withIosSemanticColors)(config, iosSemanticColors) : config;
|
|
11
|
+
// Read at prebuild from the shipped package.json, beside `build/` — outside `rootDir`, so not an import.
|
|
12
|
+
const pkg = require('../package.json');
|
|
13
|
+
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzblj/kara-expo-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The optional Expo config plugin of @hzblj/kara-ui: redefines UIKit semantic colours app-wide, natively.",
|
|
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
|
+
"./app.plugin.js": "./app.plugin.js",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"build",
|
|
33
|
+
"app.plugin.js"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p ./tsconfig.json",
|
|
37
|
+
"clean": "rm -rf build",
|
|
38
|
+
"prepack": "yarn run -T turbo run build --filter=$npm_package_name...",
|
|
39
|
+
"lint": "yarn run -T concurrently \"yarn biome:check\" \"yarn tsc:check\"",
|
|
40
|
+
"lint:fix": "yarn run -T biome check --write .",
|
|
41
|
+
"biome:check": "yarn run -T biome check .",
|
|
42
|
+
"tsc:check": "tsc -p ./tsconfig.json --noEmit"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"expo": "*"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"expo": "~57.0.22",
|
|
50
|
+
"typescript": "~7.0.2"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
6
55
|
}
|