@oneplatformdev/react-native-uikit 0.0.0-stage → 1.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.
Files changed (68) hide show
  1. package/README.md +376 -2
  2. package/bin/cli.cjs +372 -0
  3. package/package.json +34 -4
  4. package/registry/manifest.json +113 -0
  5. package/src/components/BottomModal/index.ts +2 -0
  6. package/src/components/BottomModal/presenters/useBottomModal.ts +114 -0
  7. package/src/components/BottomModal/presenters/useBottomModalInsets.ts +19 -0
  8. package/src/components/BottomModal/types/types.ts +23 -0
  9. package/src/components/BottomModal/ui/BottomModal.tsx +110 -0
  10. package/src/components/BottomModal/ui/styles.ts +115 -0
  11. package/src/components/Button/index.ts +8 -0
  12. package/src/components/Button/presenters/useButton.ts +72 -0
  13. package/src/components/Button/types/types.ts +39 -0
  14. package/src/components/Button/ui/Button.tsx +121 -0
  15. package/src/components/Button/ui/styles.ts +82 -0
  16. package/src/components/ConnectionContainer/index.ts +2 -0
  17. package/src/components/ConnectionContainer/presenters/useConnectionContainer.ts +77 -0
  18. package/src/components/ConnectionContainer/types/types.ts +14 -0
  19. package/src/components/ConnectionContainer/ui/ConnectionContainer.tsx +51 -0
  20. package/src/components/ConnectionContainer/ui/styles.ts +35 -0
  21. package/src/components/CustomAlert/index.ts +2 -0
  22. package/src/components/CustomAlert/presenters/useCustomAlert.ts +24 -0
  23. package/src/components/CustomAlert/types/types.ts +20 -0
  24. package/src/components/CustomAlert/ui/CustomAlert.tsx +77 -0
  25. package/src/components/CustomAlert/ui/styles.ts +71 -0
  26. package/src/components/HeaderWithBackButton/index.ts +2 -0
  27. package/src/components/HeaderWithBackButton/presenters/useHeaderWithBackButton.ts +17 -0
  28. package/src/components/HeaderWithBackButton/types/types.ts +21 -0
  29. package/src/components/HeaderWithBackButton/ui/HeaderWithBackButton.tsx +90 -0
  30. package/src/components/HeaderWithBackButton/ui/styles.ts +58 -0
  31. package/src/components/Input/index.ts +8 -0
  32. package/src/components/Input/presenters/useInput.ts +104 -0
  33. package/src/components/Input/types/types.ts +36 -0
  34. package/src/components/Input/ui/Input.tsx +161 -0
  35. package/src/components/Input/ui/styles.ts +80 -0
  36. package/src/components/Loader/index.ts +2 -0
  37. package/src/components/Loader/types/types.ts +12 -0
  38. package/src/components/Loader/ui/Loader.tsx +29 -0
  39. package/src/components/Loader/ui/styles.ts +14 -0
  40. package/src/components/Logger/index.ts +2 -0
  41. package/src/components/Logger/presenters/useLogger.ts +13 -0
  42. package/src/components/Logger/types/types.ts +56 -0
  43. package/src/components/Logger/ui/Logger.tsx +85 -0
  44. package/src/components/Logger/ui/components/LoggerItem.tsx +117 -0
  45. package/src/components/Logger/ui/styles.ts +121 -0
  46. package/src/components/Logger/utils/groupLoggerEntries.ts +40 -0
  47. package/src/components/Logger/utils/loggerPresentation.ts +69 -0
  48. package/src/components/ScreenContainer/index.ts +2 -0
  49. package/src/components/ScreenContainer/presenters/useScreenContainer.ts +12 -0
  50. package/src/components/ScreenContainer/types/types.ts +19 -0
  51. package/src/components/ScreenContainer/ui/ScreenContainer.tsx +83 -0
  52. package/src/components/ScreenContainer/ui/styles.ts +31 -0
  53. package/src/components/Typography/index.ts +2 -0
  54. package/src/components/Typography/types/types.ts +14 -0
  55. package/src/components/Typography/ui/Typography.tsx +37 -0
  56. package/src/components/Typography/ui/styles.ts +26 -0
  57. package/src/index.ts +13 -0
  58. package/src/provider/LocalizationContext.tsx +14 -0
  59. package/src/provider/ModeContext.tsx +9 -0
  60. package/src/provider/index.ts +12 -0
  61. package/src/provider/types.ts +31 -0
  62. package/src/theme/ThemeProvider.tsx +7 -0
  63. package/src/theme/UIProvider.tsx +60 -0
  64. package/src/theme/createTheme.ts +66 -0
  65. package/src/theme/defaultTheme.ts +215 -0
  66. package/src/theme/index.ts +4 -0
  67. package/src/theme/types.ts +265 -0
  68. package/src/utils/index.ts +94 -0
package/bin/cli.cjs ADDED
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const manifest = require('../registry/manifest.json');
7
+ const packageInfo = require('../package.json');
8
+
9
+ const packageRoot = path.resolve(__dirname, '..');
10
+ const available = Object.keys(manifest);
11
+
12
+ const help = () => {
13
+ process.stdout.write(
14
+ `Usage: ${packageInfo.name} add <component> [--dir <path>]\n` +
15
+ `Components: ${available.join(', ')}\n` +
16
+ 'Default destination: src/components/ui/<Component>\n' +
17
+ 'Run from the consuming project root; no init step is needed.\n',
18
+ );
19
+ };
20
+
21
+ const readPackage = cwd => {
22
+ const file = path.join(cwd, 'package.json');
23
+ if (!fs.existsSync(file)) {
24
+ throw new Error(`No package.json in ${cwd}. Run this command from your project root.`);
25
+ }
26
+ try {
27
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
28
+ } catch {
29
+ throw new Error(`Cannot parse ${file}. Fix package.json before adding a component.`);
30
+ }
31
+ };
32
+
33
+ const declaredDependency = (pkg, name) =>
34
+ ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
35
+ .some(section => Object.hasOwn(pkg[section] ?? {}, name));
36
+
37
+ const importPattern = /\bfrom\s+(['"])([^'"]+)\1/g;
38
+ const namedImportPattern = /\bimport\s+(?:type\s+)?\{([^}]+)\}\s+from\s+(['"])([^'"]+)\2/g;
39
+ const mergeableImportPattern = /^import\s+(type\s+)?(?:([A-Za-z_$][\w$]*)\s*,\s*)?\{([^}]*)\}\s+from\s+(['"])([^'"]+)\4;[ \t]*\n?/gm;
40
+ const moduleImportPattern = /^import\s+(?:type\s+)?(?:[A-Za-z_$][\w$]*\s*,\s*)?(?:\{[^}]*\}|\*\s+as\s+[A-Za-z_$][\w$]*|[A-Za-z_$][\w$]*)\s+from\s+(['"])([^'"]+)\1;[ \t]*$/gm;
41
+ const sideEffectImportPattern = /^import\s+(['"])([^'"]+)\1;[ \t]*$/gm;
42
+
43
+ const consolidateImports = source => {
44
+ const matches = [...source.matchAll(mergeableImportPattern)];
45
+ const groups = new Map();
46
+ for (const match of matches) {
47
+ const group = groups.get(match[5]) ?? [];
48
+ group.push(match);
49
+ groups.set(match[5], group);
50
+ }
51
+ const replacements = new Map();
52
+ for (const group of groups.values()) {
53
+ if (group.length < 2) continue;
54
+ const defaults = group.filter(match => match[2]);
55
+ const allTypeOnly = group.every(match => match[1]);
56
+ if (defaults.length > 1 || defaults.some(match => match[1]) && !allTypeOnly) continue;
57
+ const specifiers = group.flatMap(match => match[3].split(',').map(part => part.trim()).filter(Boolean)
58
+ .map(part => match[1] && !allTypeOnly && !part.startsWith('type ') ? `type ${part}` : part));
59
+ const first = group[0];
60
+ const defaultImport = defaults[0] ? `${defaults[0][2]}, ` : '';
61
+ const ending = first[0].endsWith('\n') ? '\n' : '';
62
+ replacements.set(first.index,
63
+ `import ${allTypeOnly ? 'type ' : ''}${defaultImport}{\n ${specifiers.join(',\n ')},\n} from ${first[4]}${first[5]}${first[4]};${ending}`);
64
+ for (const match of group.slice(1)) replacements.set(match.index, '');
65
+ }
66
+ let cursor = 0;
67
+ let result = '';
68
+ for (const match of matches) {
69
+ result += source.slice(cursor, match.index) + (replacements.get(match.index) ?? match[0]);
70
+ cursor = match.index + match[0].length;
71
+ }
72
+ return result + source.slice(cursor);
73
+ };
74
+
75
+ const validateUniqueImports = (name, file, source) => {
76
+ const seen = new Set();
77
+ for (const match of [...source.matchAll(moduleImportPattern), ...source.matchAll(sideEffectImportPattern)]) {
78
+ if (seen.has(match[2])) {
79
+ throw new Error(`Duplicate generated import from ${match[2]} in ${name}/${file}.`);
80
+ }
81
+ seen.add(match[2]);
82
+ }
83
+ };
84
+
85
+ const resolveComponents = name => {
86
+ const ordered = [];
87
+ const visiting = new Set();
88
+ const visited = new Set();
89
+ const visit = current => {
90
+ const entry = manifest[current];
91
+ if (!entry || !Array.isArray(entry.componentDependencies)) {
92
+ throw new Error(`Invalid component dependency in registry: ${current}.`);
93
+ }
94
+ if (visiting.has(current)) {
95
+ throw new Error(`Component dependency cycle: ${[...visiting, current].join(' -> ')}.`);
96
+ }
97
+ if (visited.has(current)) return;
98
+ visiting.add(current);
99
+ for (const dependency of entry.componentDependencies) visit(dependency);
100
+ visiting.delete(current);
101
+ visited.add(current);
102
+ ordered.push(current);
103
+ };
104
+ visit(name);
105
+ return ordered;
106
+ };
107
+
108
+ const publicContract = (name, readFile) => {
109
+ const entry = manifest[name];
110
+ if (!entry.publicExports) return;
111
+ const index = readFile('index.ts');
112
+ if (index === null) throw new Error(`Missing public entrypoint for ${name}: index.ts.`);
113
+ for (const [symbol, file] of Object.entries(entry.publicExports)) {
114
+ if (!entry.files.includes(file)) throw new Error(`Invalid public export file in ${name}: ${file}.`);
115
+ const source = readFile(file);
116
+ const moduleName = file.replace(/\.[^.]+$/, '');
117
+ if (source === null || !new RegExp(`\\bexport\\s+(?:const|function|class|type|interface)\\s+${symbol}\\b`).test(source) ||
118
+ !index.includes(`export * from './${moduleName}'`)) {
119
+ throw new Error(`missing public export ${symbol} in ${name}`);
120
+ }
121
+ }
122
+ };
123
+
124
+ const renderComponent = name => {
125
+ const entry = manifest[name];
126
+ if (!entry || !/^[A-Z][A-Za-z]+$/.test(entry.folder) || !Array.isArray(entry.files) || !entry.files.length ||
127
+ !Array.isArray(entry.componentDependencies) || !Array.isArray(entry.dependencies) || !entry.rewrites) {
128
+ throw new Error(`Invalid registry entry for ${name}.`);
129
+ }
130
+ const files = new Map();
131
+ const rewriteHits = new Set();
132
+ const requiredComponentImports = [];
133
+
134
+ for (const file of entry.files) {
135
+ if (file.split('/').some(part => !/^[A-Za-z][A-Za-z0-9.]*$/.test(part)) || files.has(file)) {
136
+ throw new Error(`Invalid source file in ${name} registry: ${file}`);
137
+ }
138
+ const sourcePath = path.join(packageRoot, 'src', 'components', entry.folder, file);
139
+ if (!fs.statSync(sourcePath, { throwIfNoEntry: false })?.isFile()) {
140
+ throw new Error(`Missing canonical source: ${sourcePath}`);
141
+ }
142
+ let source = fs.readFileSync(sourcePath, 'utf8');
143
+ for (const imported of source.matchAll(namedImportPattern)) {
144
+ const target = path.posix.normalize(path.posix.join(entry.folder, path.posix.dirname(file), imported[3]));
145
+ const dependency = entry.componentDependencies.find(current => manifest[current]?.folder === target);
146
+ if (dependency) requiredComponentImports.push({ file, declaration: imported[0], dependency });
147
+ }
148
+ source = source.replace(importPattern, (match, quote, specifier) => {
149
+ const replacement = entry.rewrites[specifier];
150
+ if (!replacement) return match;
151
+ rewriteHits.add(specifier);
152
+ return match.replace(`${quote}${specifier}${quote}`, `${quote}${replacement}${quote}`);
153
+ });
154
+ source = consolidateImports(source);
155
+ if (file === `ui/${entry.folder}.tsx`) {
156
+ source = '// Copied from @oneplatformdev/react-native-uikit. Project-owned; package upgrades do not update this file.\n' + source;
157
+ }
158
+ files.set(file, source);
159
+ validateUniqueImports(name, file, source);
160
+ }
161
+
162
+ for (const specifier of Object.keys(entry.rewrites)) {
163
+ if (!rewriteHits.has(specifier)) {
164
+ throw new Error(`Stale ${name} import rewrite: ${specifier}. Update the registry before packing.`);
165
+ }
166
+ }
167
+ for (const dependency of entry.componentDependencies) {
168
+ if (!requiredComponentImports.some(imported => imported.dependency === dependency)) {
169
+ throw new Error(`Missing canonical component import from ${name} to ${dependency}.`);
170
+ }
171
+ }
172
+ for (const imported of requiredComponentImports) {
173
+ if (!files.get(imported.file).includes(imported.declaration)) {
174
+ throw new Error(`Generated ${name}/${imported.file} no longer imports local ${imported.dependency}.`);
175
+ }
176
+ }
177
+
178
+ for (const foundation of entry.foundations) {
179
+ if (![...files.values()].some(source => new RegExp(`\\b${foundation}\\b`).test(source))) {
180
+ throw new Error(`Stale ${name} foundation metadata: ${foundation}`);
181
+ }
182
+ }
183
+ publicContract(name, file => files.get(file) ?? null);
184
+ return { entry, files };
185
+ };
186
+
187
+ const validateImports = (name, rendered) => {
188
+ const { entry, files } = rendered.get(name);
189
+ const closure = new Set(resolveComponents(name));
190
+ for (const [file, source] of files) {
191
+ for (const match of source.matchAll(importPattern)) {
192
+ const specifier = match[2];
193
+ if (!specifier.startsWith('.')) {
194
+ if (!entry.dependencies.includes(specifier)) {
195
+ throw new Error(`Undeclared generated dependency in ${name}/${file}: ${specifier}`);
196
+ }
197
+ continue;
198
+ }
199
+ const target = path.posix.normalize(path.posix.join(entry.folder, path.posix.dirname(file), specifier));
200
+ const resolved = [...rendered].find(([, component]) =>
201
+ [`${target}.ts`, `${target}.tsx`, `${target}.js`, `${target}/index.ts`]
202
+ .some(candidate => component.files.has(path.posix.relative(component.entry.folder, candidate))));
203
+ if (!resolved || !closure.has(resolved[0])) {
204
+ throw new Error(`Unresolved generated import in ${name}/${file}: ${specifier}`);
205
+ }
206
+ if (resolved[0] !== name) {
207
+ for (const imported of source.matchAll(namedImportPattern)) {
208
+ if (imported[3] !== specifier) continue;
209
+ for (const symbol of imported[1].split(',').map(part => part.trim().split(/\s+as\s+/)[0])) {
210
+ if (!manifest[resolved[0]].publicExports?.[symbol]) {
211
+ throw new Error(`Missing dependency export ${symbol} for ${name}/${file}: ${specifier}`);
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+ };
219
+
220
+ const validateRegistry = () => {
221
+ for (const name of available) resolveComponents(name);
222
+ const rendered = new Map(available.map(name => [name, renderComponent(name)]));
223
+ for (const name of available) validateImports(name, rendered);
224
+ return available.length;
225
+ };
226
+
227
+ const parseArgs = args => {
228
+ if (args.length === 1 && args[0] === '--help' || args.length === 2 && args[0] === 'add' && args[1] === '--help') {
229
+ return { help: true };
230
+ }
231
+ if (args[0] !== 'add' || !args[1] || args[1].startsWith('-')) {
232
+ throw new Error('Expected add <component>. Use --help for usage.');
233
+ }
234
+ const name = args[1].toLowerCase();
235
+ if (!manifest[name]) {
236
+ throw new Error(`Unknown component "${args[1]}". Available: ${available.join(', ')}.`);
237
+ }
238
+ let dir = 'src/components/ui';
239
+ let seenDir = false;
240
+ for (let index = 2; index < args.length; index++) {
241
+ if (args[index] !== '--dir' || seenDir) {
242
+ throw new Error(`Unexpected argument "${args[index]}". Use --help for usage.`);
243
+ }
244
+ const value = args[++index];
245
+ if (!value || value.startsWith('--')) {
246
+ throw new Error('--dir requires a relative destination path.');
247
+ }
248
+ dir = value;
249
+ seenDir = true;
250
+ }
251
+ return { name, dir };
252
+ };
253
+
254
+ const existingPath = file => {
255
+ try {
256
+ return fs.lstatSync(file);
257
+ } catch (error) {
258
+ if (error.code === 'ENOENT') return null;
259
+ throw error;
260
+ }
261
+ };
262
+
263
+ const destination = (cwd, dir, folder) => {
264
+ if (path.isAbsolute(dir)) throw new Error('--dir must be relative to the project root.');
265
+ const root = path.resolve(cwd, dir);
266
+ const relative = path.relative(cwd, root);
267
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
268
+ throw new Error('--dir must stay inside the consuming project.');
269
+ }
270
+ let current = cwd;
271
+ for (const part of relative.split(path.sep).filter(Boolean)) {
272
+ current = path.join(current, part);
273
+ const stat = existingPath(current);
274
+ if (stat?.isSymbolicLink()) throw new Error(`Destination contains a symlink: ${current}`);
275
+ if (stat && !stat.isDirectory()) throw new Error(`Destination parent is not a directory: ${current}`);
276
+ }
277
+ return { root, folderPath: path.join(root, folder) };
278
+ };
279
+
280
+ const checkExistingDependency = (name, folderPath) => {
281
+ const entry = manifest[name];
282
+ try {
283
+ if (!existingPath(folderPath)?.isDirectory()) throw new Error('not a directory');
284
+ for (const file of entry.files) {
285
+ if (!existingPath(path.join(folderPath, file))?.isFile()) throw new Error(`missing ${file}`);
286
+ }
287
+ publicContract(name, file => fs.readFileSync(path.join(folderPath, file), 'utf8'));
288
+ } catch (error) {
289
+ throw new Error(`Incompatible owned dependency ${name} at ${folderPath}: ${error.message}. No files were installed.`);
290
+ }
291
+ };
292
+
293
+ const install = (cwd, name, dir) => {
294
+ const ordered = resolveComponents(name);
295
+ const rendered = new Map(ordered.map(current => [current, renderComponent(current)]));
296
+ for (const current of ordered) validateImports(current, rendered);
297
+ const consumer = readPackage(cwd);
298
+ const missingDependencies = [...new Set(ordered.flatMap(current => rendered.get(current).entry.dependencies))]
299
+ .filter(dependency => !declaredDependency(consumer, dependency));
300
+ if (missingDependencies.length) {
301
+ throw new Error(`Declare required dependencies in your project package.json before adding ${name}: ${missingDependencies.join(', ')}. No packages were installed.`);
302
+ }
303
+ const targets = ordered.map(current => ({
304
+ name: current,
305
+ ...rendered.get(current),
306
+ ...destination(cwd, dir, rendered.get(current).entry.folder),
307
+ }));
308
+ const root = targets[0].root;
309
+ for (const target of targets) {
310
+ const existing = existingPath(target.folderPath);
311
+ if (!existing) continue;
312
+ if (target.name === name) {
313
+ throw new Error(`Destination already exists: ${target.folderPath}. Refusing to overwrite project-owned source.`);
314
+ }
315
+ checkExistingDependency(target.name, target.folderPath);
316
+ }
317
+ const pending = targets.filter(target => !existingPath(target.folderPath));
318
+
319
+ const stage = fs.mkdtempSync(path.join(cwd, '.nltdev-uikit-'));
320
+ const parents = [];
321
+ const created = [];
322
+ let current = root;
323
+ while (current !== cwd && !existingPath(current)) {
324
+ parents.push(current);
325
+ current = path.dirname(current);
326
+ }
327
+ try {
328
+ for (const target of pending) {
329
+ const stagedFolder = path.join(stage, target.entry.folder);
330
+ fs.mkdirSync(stagedFolder);
331
+ for (const [file, source] of target.files) {
332
+ const stagedFile = path.join(stagedFolder, file);
333
+ fs.mkdirSync(path.dirname(stagedFile), { recursive: true });
334
+ fs.writeFileSync(stagedFile, source, { flag: 'wx' });
335
+ }
336
+ }
337
+ for (const parent of parents.reverse()) fs.mkdirSync(parent);
338
+ for (const target of pending) {
339
+ if (existingPath(target.folderPath)) throw new Error(`Destination appeared during installation: ${target.folderPath}`);
340
+ fs.renameSync(path.join(stage, target.entry.folder), target.folderPath);
341
+ created.push(target.folderPath);
342
+ }
343
+ } catch (error) {
344
+ for (const folder of created.reverse()) fs.rmSync(folder, { recursive: true, force: true });
345
+ for (const parent of parents.reverse()) {
346
+ try { fs.rmdirSync(parent); } catch { /* Keep non-empty directories. */ }
347
+ }
348
+ throw error;
349
+ } finally {
350
+ fs.rmSync(stage, { recursive: true, force: true });
351
+ }
352
+
353
+ for (const target of targets) {
354
+ process.stdout.write(target.name === name || pending.includes(target)
355
+ ? `Installed ${target.name} to ${target.folderPath}\nFiles: ${[...target.files.keys()].join(', ')}\n`
356
+ : `Reused existing ${target.name} at ${target.folderPath} (unchanged).\n`);
357
+ }
358
+ process.stdout.write(`Source version: ${packageInfo.version}. This source is project-owned; package upgrades do not update it.\n`);
359
+ };
360
+
361
+ if (require.main === module) {
362
+ try {
363
+ const parsed = parseArgs(process.argv.slice(2));
364
+ if (parsed.help) help();
365
+ else install(process.cwd(), parsed.name, parsed.dir);
366
+ } catch (error) {
367
+ process.stderr.write(`${packageInfo.name}: ${error.message}\n`);
368
+ process.exitCode = 1;
369
+ }
370
+ }
371
+
372
+ module.exports = { validateRegistry };
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@oneplatformdev/react-native-uikit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "main": "src/index.ts",
5
+ "types": "src/index.ts",
6
+ "bin": {
7
+ "nltdev-uikit": "bin/cli.cjs"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "bin",
12
+ "registry",
13
+ "README.md",
14
+ "package.json"
15
+ ],
16
+ "scripts": {
17
+ "typecheck": "tsc --noEmit",
18
+ "validate:registry": "node scripts/validate-registry.cjs"
19
+ },
20
+ "peerDependencies": {
21
+ "react": "*",
22
+ "react-native": "*",
23
+ "react-native-keyboard-controller": "^1.22.5",
24
+ "react-native-linear-gradient": "^2.8.3",
25
+ "react-native-reanimated": "^4.6.0",
26
+ "react-native-safe-area-context": "^5.5.2"
27
+ },
28
+ "devDependencies": {
29
+ "@types/react": "^19.2.0",
30
+ "react-native-keyboard-controller": "^1.22.5",
31
+ "react-native-linear-gradient": "^2.8.3",
32
+ "react-native-reanimated": "^4.6.0",
33
+ "react-native-safe-area-context": "^5.5.2",
34
+ "typescript": "^5.0.0"
35
+ }
36
+ }
@@ -0,0 +1,113 @@
1
+ {
2
+ "typography": {
3
+ "folder": "Typography",
4
+ "files": ["ui/Typography.tsx", "ui/styles.ts", "types/types.ts", "index.ts"],
5
+ "componentDependencies": [],
6
+ "publicExports": { "Typography": "ui/Typography.tsx", "TypographyProps": "types/types.ts" },
7
+ "foundations": ["useUITheme", "useScaling", "UIColorKey", "UIFontKey", "TypographyVariant", "UITypographyToken"],
8
+ "dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
9
+ "rewrites": {
10
+ "../../../theme": "@oneplatformdev/react-native-uikit",
11
+ "../../../utils": "@oneplatformdev/react-native-uikit"
12
+ }
13
+ },
14
+ "button": {
15
+ "folder": "Button",
16
+ "files": ["ui/Button.tsx", "presenters/useButton.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
17
+ "componentDependencies": ["typography"],
18
+ "foundations": ["useUITheme", "useScaling", "Typography", "TypographyProps", "UIColors", "UIButtonSizeTheme", "UIButtonVariantTheme", "ScalingFunctions"],
19
+ "dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
20
+ "rewrites": {
21
+ "../../../theme": "@oneplatformdev/react-native-uikit",
22
+ "../../../utils": "@oneplatformdev/react-native-uikit"
23
+ }
24
+ },
25
+ "input": {
26
+ "folder": "Input",
27
+ "files": ["ui/Input.tsx", "presenters/useInput.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
28
+ "componentDependencies": ["typography"],
29
+ "foundations": ["useUITheme", "useScaling", "Typography", "TypographyProps", "UIColors", "UIFonts", "UIInputTheme", "ScalingFunctions"],
30
+ "dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
31
+ "rewrites": {
32
+ "../../../theme": "@oneplatformdev/react-native-uikit",
33
+ "../../../utils": "@oneplatformdev/react-native-uikit"
34
+ }
35
+ },
36
+ "screen-container": {
37
+ "folder": "ScreenContainer",
38
+ "files": ["ui/ScreenContainer.tsx", "presenters/useScreenContainer.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
39
+ "componentDependencies": [],
40
+ "foundations": ["useUITheme", "useScaling", "UIColors", "UISpacing", "ScalingFunctions"],
41
+ "dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit", "react-native-keyboard-controller", "react-native-linear-gradient", "react-native-safe-area-context"],
42
+ "rewrites": {
43
+ "../../../theme": "@oneplatformdev/react-native-uikit",
44
+ "../../../utils": "@oneplatformdev/react-native-uikit"
45
+ }
46
+ },
47
+ "custom-alert": {
48
+ "folder": "CustomAlert",
49
+ "files": ["ui/CustomAlert.tsx", "presenters/useCustomAlert.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
50
+ "componentDependencies": ["typography"],
51
+ "foundations": ["useUITheme", "useScaling", "Typography", "UIColors", "UISpacing", "UIRadius", "ScalingFunctions"],
52
+ "dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
53
+ "rewrites": {
54
+ "../../../theme": "@oneplatformdev/react-native-uikit",
55
+ "../../../utils": "@oneplatformdev/react-native-uikit"
56
+ }
57
+ },
58
+ "bottom-modal": {
59
+ "folder": "BottomModal",
60
+ "files": ["ui/BottomModal.tsx", "presenters/useBottomModal.ts", "presenters/useBottomModalInsets.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
61
+ "componentDependencies": ["typography"],
62
+ "foundations": ["useUITheme", "useScaling", "Typography", "UIColors", "UISpacing", "UIRadius", "ScalingFunctions"],
63
+ "dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit", "react-native-keyboard-controller", "react-native-reanimated", "react-native-safe-area-context"],
64
+ "rewrites": {
65
+ "../../../theme": "@oneplatformdev/react-native-uikit",
66
+ "../../../utils": "@oneplatformdev/react-native-uikit"
67
+ }
68
+ },
69
+ "connection-container": {
70
+ "folder": "ConnectionContainer",
71
+ "files": ["ui/ConnectionContainer.tsx", "presenters/useConnectionContainer.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
72
+ "componentDependencies": ["typography"],
73
+ "foundations": ["useUITheme", "useScaling", "Typography", "UIColors", "UISpacing", "ScalingFunctions"],
74
+ "dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit", "react-native-safe-area-context"],
75
+ "rewrites": {
76
+ "../../../theme": "@oneplatformdev/react-native-uikit",
77
+ "../../../utils": "@oneplatformdev/react-native-uikit"
78
+ }
79
+ },
80
+ "header-with-back-button": {
81
+ "folder": "HeaderWithBackButton",
82
+ "files": ["ui/HeaderWithBackButton.tsx", "ui/styles.ts", "presenters/useHeaderWithBackButton.ts", "types/types.ts", "index.ts"],
83
+ "componentDependencies": ["typography"],
84
+ "foundations": ["useUITheme", "useScaling", "Typography", "TypographyProps", "UIColors", "UISpacing", "ScalingFunctions"],
85
+ "dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
86
+ "rewrites": {
87
+ "../../../theme": "@oneplatformdev/react-native-uikit",
88
+ "../../../utils": "@oneplatformdev/react-native-uikit"
89
+ }
90
+ },
91
+ "loader": {
92
+ "folder": "Loader",
93
+ "files": ["ui/Loader.tsx", "ui/styles.ts", "types/types.ts", "index.ts"],
94
+ "componentDependencies": [],
95
+ "foundations": ["useUITheme", "UIColors"],
96
+ "dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
97
+ "rewrites": {
98
+ "../../../theme": "@oneplatformdev/react-native-uikit"
99
+ }
100
+ },
101
+ "logger": {
102
+ "folder": "Logger",
103
+ "files": ["ui/Logger.tsx", "ui/components/LoggerItem.tsx", "utils/groupLoggerEntries.ts", "utils/loggerPresentation.ts", "presenters/useLogger.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
104
+ "componentDependencies": ["typography"],
105
+ "foundations": ["useUITheme", "useScaling", "Typography", "UIColors", "UIRadius", "UISpacing", "ScalingFunctions"],
106
+ "dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit", "react-native-safe-area-context"],
107
+ "rewrites": {
108
+ "../../../theme": "@oneplatformdev/react-native-uikit",
109
+ "../../../../theme": "@oneplatformdev/react-native-uikit",
110
+ "../../../utils": "@oneplatformdev/react-native-uikit"
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,2 @@
1
+ export { BottomModal } from './ui/BottomModal';
2
+ export type { BottomModalCloseAction, BottomModalProps } from './types/types';
@@ -0,0 +1,114 @@
1
+ import { useCallback, useEffect, useMemo } from 'react';
2
+ import { Animated, Easing, Keyboard, PanResponder, useWindowDimensions } from 'react-native';
3
+ import type { BottomModalProps } from '../types/types';
4
+ import { useBottomModalInsets } from './useBottomModalInsets';
5
+
6
+ type UseBottomModalProps = Pick<BottomModalProps, 'onClose' | 'closeRef'>;
7
+
8
+ const CLOSE_DISTANCE = 120;
9
+ const CLOSE_VELOCITY = 1;
10
+ const OPEN_ANIMATION_DURATION = 280;
11
+ const CLOSE_ANIMATION_DURATION = 250;
12
+
13
+ const isDragDownGesture = (gestureState: { dx: number; dy: number }) =>
14
+ gestureState.dy > 0 && Math.abs(gestureState.dy) > Math.abs(gestureState.dx);
15
+
16
+ export const useBottomModal = ({
17
+ onClose,
18
+ closeRef,
19
+ }: UseBottomModalProps) => {
20
+ const { height } = useWindowDimensions();
21
+ const { topInset, bottomInset } = useBottomModalInsets();
22
+ const translateY = useMemo(() => new Animated.Value(height), [height]);
23
+ const backdropOpacity = useMemo(() => new Animated.Value(0), []);
24
+
25
+ const onRunCloseAnimation = useCallback(
26
+ (shouldNotify = true) => {
27
+ translateY.stopAnimation();
28
+ backdropOpacity.stopAnimation();
29
+ Animated.parallel([
30
+ Animated.timing(translateY, {
31
+ toValue: height,
32
+ duration: CLOSE_ANIMATION_DURATION,
33
+ easing: Easing.in(Easing.cubic),
34
+ useNativeDriver: true,
35
+ }),
36
+ Animated.timing(backdropOpacity, {
37
+ toValue: 0,
38
+ duration: CLOSE_ANIMATION_DURATION,
39
+ easing: Easing.in(Easing.cubic),
40
+ useNativeDriver: true,
41
+ }),
42
+ ]).start(({ finished }) => {
43
+ if (finished && shouldNotify) onClose();
44
+ });
45
+ },
46
+ [backdropOpacity, height, onClose, translateY],
47
+ );
48
+ const onCloseAnimated = useCallback(
49
+ (shouldNotify = true) => {
50
+ Keyboard.dismiss();
51
+ onRunCloseAnimation(shouldNotify);
52
+ },
53
+ [onRunCloseAnimation],
54
+ );
55
+ const onClosePress = useCallback(() => onCloseAnimated(), [onCloseAnimated]);
56
+ const onShow = useCallback(() => {
57
+ translateY.stopAnimation();
58
+ backdropOpacity.stopAnimation();
59
+ translateY.setValue(height);
60
+ backdropOpacity.setValue(0);
61
+ Animated.parallel([
62
+ Animated.timing(translateY, {
63
+ toValue: 0,
64
+ duration: OPEN_ANIMATION_DURATION,
65
+ easing: Easing.out(Easing.cubic),
66
+ useNativeDriver: true,
67
+ }),
68
+ Animated.timing(backdropOpacity, {
69
+ toValue: 1,
70
+ duration: OPEN_ANIMATION_DURATION,
71
+ easing: Easing.out(Easing.cubic),
72
+ useNativeDriver: true,
73
+ }),
74
+ ]).start();
75
+ }, [backdropOpacity, height, translateY]);
76
+
77
+ useEffect(() => {
78
+ if (!closeRef) return;
79
+ closeRef.current = onClosePress;
80
+ return () => { closeRef.current = null; };
81
+ }, [closeRef, onClosePress]);
82
+
83
+ const panResponder = useMemo(
84
+ () => PanResponder.create({
85
+ onMoveShouldSetPanResponderCapture: (_, gestureState) => isDragDownGesture(gestureState),
86
+ onMoveShouldSetPanResponder: (_, gestureState) => isDragDownGesture(gestureState),
87
+ onPanResponderMove: (_, gestureState) => {
88
+ if (gestureState.dy > 0) translateY.setValue(gestureState.dy);
89
+ },
90
+ onPanResponderRelease: (_, gestureState) => {
91
+ if (gestureState.dy > CLOSE_DISTANCE || gestureState.vy > CLOSE_VELOCITY) {
92
+ onCloseAnimated();
93
+ return;
94
+ }
95
+ Animated.spring(translateY, { toValue: 0, useNativeDriver: true, bounciness: 0, speed: 18 }).start();
96
+ },
97
+ onPanResponderTerminate: () => {
98
+ Animated.spring(translateY, { toValue: 0, useNativeDriver: true, bounciness: 0, speed: 18 }).start();
99
+ },
100
+ }),
101
+ [onCloseAnimated, translateY],
102
+ );
103
+
104
+ return {
105
+ height,
106
+ topInset,
107
+ bottomInset,
108
+ translateY,
109
+ backdropOpacity,
110
+ onClosePress,
111
+ onShow,
112
+ panHandlers: panResponder.panHandlers,
113
+ };
114
+ };
@@ -0,0 +1,19 @@
1
+ import { useMemo } from 'react';
2
+ import { Dimensions, Platform } from 'react-native';
3
+ import { initialWindowMetrics, useSafeAreaInsets } from 'react-native-safe-area-context';
4
+
5
+ const getAndroidNavigationInset = () => {
6
+ if (Platform.OS !== 'android') return 0;
7
+ return Math.max(0, Dimensions.get('screen').height - Dimensions.get('window').height);
8
+ };
9
+
10
+ export const useBottomModalInsets = () => {
11
+ const { top, bottom } = useSafeAreaInsets();
12
+ const topInset = useMemo(() => Math.max(top, initialWindowMetrics?.insets.top || 0), [top]);
13
+ const bottomInset = useMemo(
14
+ () => Math.max(bottom, initialWindowMetrics?.insets.bottom || 0, getAndroidNavigationInset()),
15
+ [bottom],
16
+ );
17
+
18
+ return { topInset, bottomInset };
19
+ };