@thewhileloop/whileui 4.2.1 → 4.3.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 +1235 -663
- package/dist/agent/catalog.json +1737 -0
- package/dist/agent/llms-components.txt +2787 -0
- package/dist/agent/llms-full.txt +2962 -0
- package/dist/agent/llms.txt +152 -0
- package/dist/blocks/chat/chat.d.ts +10 -2
- package/dist/blocks/chat/chat.js +1 -1
- package/dist/blocks/chat/index.d.ts +1 -1
- package/dist/blocks/commerce/feature-gate.js +2 -2
- package/dist/blocks/commerce/metric-card.js +1 -1
- package/dist/blocks/commerce/subscription-card.d.ts +7 -0
- package/dist/blocks/commerce/usage-bar.d.ts +4 -0
- package/dist/blocks/layout/app-shell.d.ts +4 -0
- package/dist/blocks/layout/empty-state.js +1 -1
- package/dist/blocks/layout/error-state.js +1 -1
- package/dist/blocks/layout/page-skeleton.d.ts +1 -0
- package/dist/blocks/layout/pull-to-refresh-scroll-view.d.ts +1 -0
- package/dist/blocks/layout/screen-skeleton.d.ts +5 -0
- package/dist/blocks/layout/sheet.js +1 -1
- package/dist/blocks/lists/list-item.js +1 -1
- package/dist/blocks/lists/notification-item.js +1 -1
- package/dist/blocks/media/smart-image.js +4 -2
- package/dist/blocks/navigation/bottom-nav.js +1 -1
- package/dist/blocks/navigation/drawer-menu.js +2 -2
- package/dist/blocks/navigation/floating-bottom-nav.js +3 -1
- package/dist/blocks/navigation/tab-bar.js +1 -1
- package/dist/components/alert/alert.d.ts +7 -0
- package/dist/components/alert/alert.js +23 -4
- package/dist/components/button/button.d.ts +3 -0
- package/dist/components/button/button.js +2 -1
- package/dist/components/form-field/form-field.d.ts +18 -0
- package/dist/components/form-field/form-field.js +25 -4
- package/dist/components/input/input.js +2 -2
- package/dist/components/numeric-input/numeric-input.d.ts +3 -0
- package/dist/components/numeric-input/numeric-input.js +2 -2
- package/dist/components/otp-input/otp-input.d.ts +8 -0
- package/dist/components/pressable/pressable.d.ts +1 -0
- package/dist/components/segmented-control/segmented-control.d.ts +1 -0
- package/dist/components/surface/surface.d.ts +5 -0
- package/dist/components/textarea/textarea.js +2 -2
- package/dist/components/toast/toast.d.ts +10 -10
- package/dist/components/toast/toast.js +53 -35
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/frosted-surface.d.ts +3 -0
- package/dist/lib/theme-colors.d.ts +4 -3
- package/dist/lib/theme-colors.js +26 -1
- package/dist/lib/theme-contract.js +12 -12
- package/dist/lib/theme-presets.js +9 -9
- package/dist/lib/theme-seeds.d.ts +40 -0
- package/dist/lib/theme-seeds.js +143 -0
- package/dist/lib/visual-tokens.js +13 -0
- package/dist/metro.cjs +12 -5
- package/dist/tokens.d.ts +1 -0
- package/dist/tokens.js +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// A full theme from a few seed colours, plus a contrast check for any palette.
|
|
2
|
+
// Renderer-free: safe in build scripts and for agents generating themes.
|
|
3
|
+
//
|
|
4
|
+
// Seeds are never altered: a brand colour is a brand decision, so when it fails
|
|
5
|
+
// contrast the check reports it instead. Only derived colours (muted text, the text on
|
|
6
|
+
// each filled colour, status colours) are tuned to meet WCAG.
|
|
7
|
+
import { blend, converter, formatHex, parse, wcagContrast } from 'culori';
|
|
8
|
+
import { createThemePreset } from './theme-presets';
|
|
9
|
+
const toOklch = converter('oklch');
|
|
10
|
+
/** WCAG 2.x contrast ratio between two CSS colours, 1–21. */
|
|
11
|
+
function contrastRatio(a, b) {
|
|
12
|
+
return wcagContrast(a, b);
|
|
13
|
+
}
|
|
14
|
+
function oklch(color) {
|
|
15
|
+
const parsed = parse(color);
|
|
16
|
+
if (!parsed)
|
|
17
|
+
throw new Error(`Not a CSS colour: "${color}"`);
|
|
18
|
+
return toOklch(parsed);
|
|
19
|
+
}
|
|
20
|
+
const hex = (l, c, h) => formatHex({ mode: 'oklch', l: Math.min(1, Math.max(0, l)), c, h });
|
|
21
|
+
/** Mix `a` toward `b` by `t` in OKLCH lightness and chroma, keeping `a`'s hue. */
|
|
22
|
+
function toward(a, b, t) {
|
|
23
|
+
const x = oklch(a);
|
|
24
|
+
const y = oklch(b);
|
|
25
|
+
return hex(x.l + (y.l - x.l) * t, (x.c ?? 0) + ((y.c ?? 0) - (x.c ?? 0)) * t, x.h ?? y.h ?? 0);
|
|
26
|
+
}
|
|
27
|
+
/** Move `color` away from `against` in lightness until it reaches `min` contrast. */
|
|
28
|
+
function ensureContrast(color, against, min) {
|
|
29
|
+
const c = oklch(color);
|
|
30
|
+
const darker = oklch(against).l > 0.5;
|
|
31
|
+
for (let l = c.l; l >= 0 && l <= 1; l += darker ? -0.01 : 0.01) {
|
|
32
|
+
const candidate = hex(l, c.c ?? 0, c.h ?? 0);
|
|
33
|
+
if (contrastRatio(candidate, against) >= min)
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
return darker ? '#000000' : '#ffffff';
|
|
37
|
+
}
|
|
38
|
+
/** Black or white tinted toward the theme, whichever reads better on `fill`. */
|
|
39
|
+
function onFill(fill, seeds) {
|
|
40
|
+
const light = oklch(seeds.background).l > oklch(seeds.foreground).l ? seeds.background : seeds.foreground;
|
|
41
|
+
const dark = light === seeds.background ? seeds.foreground : seeds.background;
|
|
42
|
+
return contrastRatio(light, fill) >= contrastRatio(dark, fill) ? light : dark;
|
|
43
|
+
}
|
|
44
|
+
// Status hues stay recognisable across brands; only their lightness adapts to the page.
|
|
45
|
+
const STATUS_HUES = { destructive: 27, success: 150, warning: 75, info: 240 };
|
|
46
|
+
/** The 25 base colours a theme needs, derived from its seeds. */
|
|
47
|
+
function themeBaseFromSeeds(seeds) {
|
|
48
|
+
const { background, foreground, primary } = seeds;
|
|
49
|
+
const dark = oklch(background).l < 0.5;
|
|
50
|
+
const accent = seeds.accent ?? toward(primary, background, 0.82);
|
|
51
|
+
const surface = toward(background, foreground, dark ? 0.06 : 0.025);
|
|
52
|
+
const muted = toward(background, foreground, dark ? 0.1 : 0.06);
|
|
53
|
+
const status = (hue) => ensureContrast(hex(dark ? 0.72 : 0.55, 0.15, hue), background, 4.5);
|
|
54
|
+
const destructive = status(STATUS_HUES.destructive);
|
|
55
|
+
const success = status(STATUS_HUES.success);
|
|
56
|
+
const warning = status(STATUS_HUES.warning);
|
|
57
|
+
const info = status(STATUS_HUES.info);
|
|
58
|
+
const fills = { background, foreground };
|
|
59
|
+
return {
|
|
60
|
+
background,
|
|
61
|
+
foreground,
|
|
62
|
+
card: surface,
|
|
63
|
+
cardForeground: foreground,
|
|
64
|
+
popover: surface,
|
|
65
|
+
popoverForeground: foreground,
|
|
66
|
+
primary,
|
|
67
|
+
primaryForeground: onFill(primary, fills),
|
|
68
|
+
secondary: muted,
|
|
69
|
+
secondaryForeground: foreground,
|
|
70
|
+
accent,
|
|
71
|
+
accentForeground: onFill(accent, fills),
|
|
72
|
+
muted,
|
|
73
|
+
mutedForeground: ensureContrast(toward(foreground, background, 0.42), muted, 4.5),
|
|
74
|
+
border: toward(background, foreground, dark ? 0.16 : 0.13),
|
|
75
|
+
input: muted,
|
|
76
|
+
ring: primary,
|
|
77
|
+
destructive,
|
|
78
|
+
destructiveForeground: onFill(destructive, fills),
|
|
79
|
+
success,
|
|
80
|
+
successForeground: onFill(success, fills),
|
|
81
|
+
warning,
|
|
82
|
+
warningForeground: onFill(warning, fills),
|
|
83
|
+
info,
|
|
84
|
+
infoForeground: onFill(info, fills),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A complete preset from seeds. `light` is required; `dark` defaults to a dark page with
|
|
89
|
+
* the same brand colours. `overrides` replaces any base colour before the rest is
|
|
90
|
+
* derived, so nothing is out of reach; derived tokens can be adjusted on the result.
|
|
91
|
+
*/
|
|
92
|
+
export function themeFromSeeds(input) {
|
|
93
|
+
const { light } = input;
|
|
94
|
+
const page = oklch(light.background);
|
|
95
|
+
const darkPage = hex(0.17, Math.min(page.c ?? 0, 0.02), page.h ?? 0);
|
|
96
|
+
// Generated, not supplied, so brand colours may be lifted until they read on a dark page.
|
|
97
|
+
const dark = input.dark ?? {
|
|
98
|
+
background: darkPage,
|
|
99
|
+
foreground: hex(0.94, Math.min(page.c ?? 0, 0.015), page.h ?? 0),
|
|
100
|
+
primary: ensureContrast(light.primary, darkPage, 4.5),
|
|
101
|
+
accent: light.accent && ensureContrast(light.accent, darkPage, 3),
|
|
102
|
+
};
|
|
103
|
+
return createThemePreset({
|
|
104
|
+
name: input.name,
|
|
105
|
+
description: input.description,
|
|
106
|
+
light: { ...themeBaseFromSeeds(light), ...input.overrides?.light },
|
|
107
|
+
dark: { ...themeBaseFromSeeds(dark), ...input.overrides?.dark },
|
|
108
|
+
visual: input.visual,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const TEXT_PAIRS = [
|
|
112
|
+
['foreground', 'background'],
|
|
113
|
+
['cardForeground', 'card'],
|
|
114
|
+
['popoverForeground', 'popover'],
|
|
115
|
+
['mutedForeground', 'background'],
|
|
116
|
+
['mutedForeground', 'muted'],
|
|
117
|
+
['primaryForeground', 'primary'],
|
|
118
|
+
['secondaryForeground', 'secondary'],
|
|
119
|
+
['accentForeground', 'accent'],
|
|
120
|
+
['destructiveForeground', 'destructive'],
|
|
121
|
+
['successForeground', 'success'],
|
|
122
|
+
['warningForeground', 'warning'],
|
|
123
|
+
['infoForeground', 'info'],
|
|
124
|
+
['primary', 'background'],
|
|
125
|
+
['destructive', 'background'],
|
|
126
|
+
];
|
|
127
|
+
const UI_PAIRS = [['ring', 'background']];
|
|
128
|
+
/** A translucent colour as it actually renders: blended onto the page. */
|
|
129
|
+
function onPage(color, page) {
|
|
130
|
+
const parsed = parse(color);
|
|
131
|
+
if (!parsed || (parsed.alpha ?? 1) >= 1)
|
|
132
|
+
return color;
|
|
133
|
+
return formatHex(blend([page, parsed], 'normal'));
|
|
134
|
+
}
|
|
135
|
+
/** Every text and focus-ring pairing the components render, checked against WCAG AA. */
|
|
136
|
+
export function checkThemeContrast(colors) {
|
|
137
|
+
const check = ([fg, bg], min) => {
|
|
138
|
+
const background = onPage(colors[bg], colors.background);
|
|
139
|
+
const ratio = Math.round(contrastRatio(onPage(colors[fg], background), background) * 100) / 100;
|
|
140
|
+
return { pair: `${fg} on ${bg}`, ratio, min, pass: ratio >= min };
|
|
141
|
+
};
|
|
142
|
+
return [...TEXT_PAIRS.map((p) => check(p, 4.5)), ...UI_PAIRS.map((p) => check(p, 3))];
|
|
143
|
+
}
|
|
@@ -28,11 +28,24 @@ function resolveVisualToken(spec, value) {
|
|
|
28
28
|
}
|
|
29
29
|
return parseRatio(value, spec.fallback, spec.min ?? 0, spec.max ?? 1);
|
|
30
30
|
}
|
|
31
|
+
// Resolved once per distinct CSS snapshot and shared, for the same reason as
|
|
32
|
+
// `useThemeColors`: 83 parses per component per render add up on a list screen, and a
|
|
33
|
+
// stable object lets callers memoise on it.
|
|
34
|
+
const resolvedVisualTokens = new Map();
|
|
35
|
+
const RESOLVED_VISUAL_LIMIT = 8;
|
|
31
36
|
export function useVisualTokens() {
|
|
32
37
|
const values = useCSSVariable(VISUAL_TOKEN_VARIABLES);
|
|
38
|
+
const key = values.join('\u0001');
|
|
39
|
+
const cached = resolvedVisualTokens.get(key);
|
|
40
|
+
if (cached)
|
|
41
|
+
return cached;
|
|
33
42
|
const resolved = {};
|
|
34
43
|
VISUAL_TOKEN_SPECS.forEach((spec, index) => {
|
|
35
44
|
resolved[spec.key] = resolveVisualToken(spec, values[index]);
|
|
36
45
|
});
|
|
46
|
+
if (resolvedVisualTokens.size >= RESOLVED_VISUAL_LIMIT) {
|
|
47
|
+
resolvedVisualTokens.delete(resolvedVisualTokens.keys().next().value);
|
|
48
|
+
}
|
|
49
|
+
resolvedVisualTokens.set(key, resolved);
|
|
37
50
|
return resolved;
|
|
38
51
|
}
|
package/dist/metro.cjs
CHANGED
|
@@ -4,14 +4,20 @@ const { withUniwindConfig } = require('uniwind/metro');
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Packages that must resolve the real `react-native`. Uniwind's native resolver
|
|
7
|
-
* rewrites every `from 'react-native'` to its component wrapper
|
|
8
|
-
* components and
|
|
9
|
-
* `undefined` with no build error. `expo-asset` imports `AssetRegistry
|
|
10
|
-
* without it every `@expo/vector-icons` glyph is silently absent on
|
|
7
|
+
* rewrites every `from 'react-native'` to its component wrapper. The wrapper re-exports
|
|
8
|
+
* the components and most of the API, but not all of it, and a missing export arrives
|
|
9
|
+
* as `undefined` with no build error. `expo-asset` imports `AssetRegistry`, which the
|
|
10
|
+
* wrapper lacks, and without it every `@expo/vector-icons` glyph is silently absent on
|
|
11
|
+
* iOS and Android.
|
|
11
12
|
*/
|
|
12
13
|
const NEEDS_REAL_REACT_NATIVE = ['expo-asset', 'expo-font', '@expo/vector-icons'];
|
|
13
14
|
|
|
14
15
|
function withWhileUIMetro(config, options) {
|
|
16
|
+
if (!options || typeof options.cssEntryFile !== 'string') {
|
|
17
|
+
throw new Error(
|
|
18
|
+
"withWhileUIMetro(config, options): pass Uniwind's options, at least { cssEntryFile: './global.css' }"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
15
21
|
const { exemptFromUniwindRewrite = [], ...uniwindOptions } = options;
|
|
16
22
|
const wrapped = withUniwindConfig(config, uniwindOptions);
|
|
17
23
|
const exempt = [...NEEDS_REAL_REACT_NATIVE, ...exemptFromUniwindRewrite];
|
|
@@ -19,7 +25,8 @@ function withWhileUIMetro(config, options) {
|
|
|
19
25
|
|
|
20
26
|
wrapped.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
21
27
|
if (moduleName === 'react-native') {
|
|
22
|
-
|
|
28
|
+
// Metro reports Windows paths with backslashes; match either separator.
|
|
29
|
+
const origin = (context.originModulePath ?? '').replace(/\\/g, '/');
|
|
23
30
|
if (exempt.some((pkg) => origin.includes(`node_modules/${pkg}/`))) {
|
|
24
31
|
return context.resolveRequest(context, moduleName, platform);
|
|
25
32
|
}
|
package/dist/tokens.d.ts
CHANGED
package/dist/tokens.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thewhileloop/whileui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "WhileUI Native — Copy-paste components for React Native. You own the code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"types": "./dist/markdown.d.ts",
|
|
53
53
|
"import": "./dist/markdown.js"
|
|
54
54
|
},
|
|
55
|
+
"./agent/catalog.json": "./dist/agent/catalog.json",
|
|
55
56
|
"./package.json": "./package.json"
|
|
56
57
|
},
|
|
57
58
|
"files": [
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
"access": "public"
|
|
64
65
|
},
|
|
65
66
|
"scripts": {
|
|
66
|
-
"build": "tsc -p tsconfig.build.json && cp src/metro.cjs src/metro.d.cts dist/",
|
|
67
|
+
"build": "tsc -p tsconfig.build.json && cp src/metro.cjs src/metro.d.cts dist/ && bun scripts/build-agent-docs.mjs",
|
|
67
68
|
"prepack": "cp ../../README.md README.md",
|
|
68
69
|
"postpack": "rm -f README.md",
|
|
69
70
|
"prepare": "bun run build",
|