@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.
Files changed (56) hide show
  1. package/README.md +1235 -663
  2. package/dist/agent/catalog.json +1737 -0
  3. package/dist/agent/llms-components.txt +2787 -0
  4. package/dist/agent/llms-full.txt +2962 -0
  5. package/dist/agent/llms.txt +152 -0
  6. package/dist/blocks/chat/chat.d.ts +10 -2
  7. package/dist/blocks/chat/chat.js +1 -1
  8. package/dist/blocks/chat/index.d.ts +1 -1
  9. package/dist/blocks/commerce/feature-gate.js +2 -2
  10. package/dist/blocks/commerce/metric-card.js +1 -1
  11. package/dist/blocks/commerce/subscription-card.d.ts +7 -0
  12. package/dist/blocks/commerce/usage-bar.d.ts +4 -0
  13. package/dist/blocks/layout/app-shell.d.ts +4 -0
  14. package/dist/blocks/layout/empty-state.js +1 -1
  15. package/dist/blocks/layout/error-state.js +1 -1
  16. package/dist/blocks/layout/page-skeleton.d.ts +1 -0
  17. package/dist/blocks/layout/pull-to-refresh-scroll-view.d.ts +1 -0
  18. package/dist/blocks/layout/screen-skeleton.d.ts +5 -0
  19. package/dist/blocks/layout/sheet.js +1 -1
  20. package/dist/blocks/lists/list-item.js +1 -1
  21. package/dist/blocks/lists/notification-item.js +1 -1
  22. package/dist/blocks/media/smart-image.js +4 -2
  23. package/dist/blocks/navigation/bottom-nav.js +1 -1
  24. package/dist/blocks/navigation/drawer-menu.js +2 -2
  25. package/dist/blocks/navigation/floating-bottom-nav.js +3 -1
  26. package/dist/blocks/navigation/tab-bar.js +1 -1
  27. package/dist/components/alert/alert.d.ts +7 -0
  28. package/dist/components/alert/alert.js +23 -4
  29. package/dist/components/button/button.d.ts +3 -0
  30. package/dist/components/button/button.js +2 -1
  31. package/dist/components/form-field/form-field.d.ts +18 -0
  32. package/dist/components/form-field/form-field.js +25 -4
  33. package/dist/components/input/input.js +2 -2
  34. package/dist/components/numeric-input/numeric-input.d.ts +3 -0
  35. package/dist/components/numeric-input/numeric-input.js +2 -2
  36. package/dist/components/otp-input/otp-input.d.ts +8 -0
  37. package/dist/components/pressable/pressable.d.ts +1 -0
  38. package/dist/components/segmented-control/segmented-control.d.ts +1 -0
  39. package/dist/components/surface/surface.d.ts +5 -0
  40. package/dist/components/textarea/textarea.js +2 -2
  41. package/dist/components/toast/toast.d.ts +10 -10
  42. package/dist/components/toast/toast.js +53 -35
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.js +1 -0
  45. package/dist/lib/frosted-surface.d.ts +3 -0
  46. package/dist/lib/theme-colors.d.ts +4 -3
  47. package/dist/lib/theme-colors.js +26 -1
  48. package/dist/lib/theme-contract.js +12 -12
  49. package/dist/lib/theme-presets.js +9 -9
  50. package/dist/lib/theme-seeds.d.ts +40 -0
  51. package/dist/lib/theme-seeds.js +143 -0
  52. package/dist/lib/visual-tokens.js +13 -0
  53. package/dist/metro.cjs +12 -5
  54. package/dist/tokens.d.ts +1 -0
  55. package/dist/tokens.js +1 -0
  56. 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, which re-exports
8
- * components and nothing else, so a package importing a non-component export gets
9
- * `undefined` with no build error. `expo-asset` imports `AssetRegistry` that way, and
10
- * without it every `@expo/vector-icons` glyph is silently absent on iOS and Android.
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
- const origin = context.originModulePath ?? '';
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
@@ -2,3 +2,4 @@ export * from './lib/theme-contract';
2
2
  export * from './lib/visual-token-contract';
3
3
  export * from './lib/theme-presets';
4
4
  export * from './lib/theme-derive';
5
+ export * from './lib/theme-seeds';
package/dist/tokens.js CHANGED
@@ -4,3 +4,4 @@ export * from './lib/theme-contract';
4
4
  export * from './lib/visual-token-contract';
5
5
  export * from './lib/theme-presets';
6
6
  export * from './lib/theme-derive';
7
+ export * from './lib/theme-seeds';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thewhileloop/whileui",
3
- "version": "4.2.1",
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",