deskforge 1.0.0 → 1.0.1

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 (84) hide show
  1. package/COMPLETION_REPORT.md +426 -0
  2. package/COMPONENT_API.md +529 -0
  3. package/DELIVERY_SUMMARY.md +455 -0
  4. package/IMPLEMENTATION_GUIDE.md +385 -0
  5. package/IMPORT_GUIDE.md +404 -0
  6. package/QUICK_REFERENCE.md +352 -0
  7. package/README.md +245 -31
  8. package/README_UI_COMPLETE.md +395 -0
  9. package/TYPE_DEFINITIONS.md +671 -0
  10. package/UI_GETTING_STARTED.md +523 -0
  11. package/examples/deskforge-app/App.ts +7 -11
  12. package/examples/deskforge-app/screens/CounterApp.ts +181 -0
  13. package/examples/deskforge-app/screens/GalleryApp.ts +267 -0
  14. package/examples/deskforge-app/screens/NotesApp.ts +376 -0
  15. package/examples/deskforge-app/screens/SettingsApp.ts +226 -0
  16. package/examples/deskforge-app/screens/TodoApp.ts +288 -0
  17. package/examples/deskforge-app/screens/index.ts +50 -0
  18. package/package.json +1 -1
  19. package/packages/cli/dist/commands/start.d.ts +7 -1
  20. package/packages/cli/dist/commands/start.d.ts.map +1 -1
  21. package/packages/cli/dist/commands/start.js +75 -21
  22. package/packages/cli/dist/commands/start.js.map +1 -1
  23. package/packages/cli/src/commands/start.ts +93 -21
  24. package/packages/cli/test/start.test.js +24 -0
  25. package/packages/core/dist/index.d.ts +1 -0
  26. package/packages/core/dist/index.d.ts.map +1 -1
  27. package/packages/core/dist/index.js +1 -0
  28. package/packages/core/dist/index.js.map +1 -1
  29. package/packages/core/dist/style/StyleObject.d.ts +115 -0
  30. package/packages/core/dist/style/StyleObject.d.ts.map +1 -0
  31. package/packages/core/dist/style/StyleObject.js +6 -0
  32. package/packages/core/dist/style/StyleObject.js.map +1 -0
  33. package/packages/core/dist/style/StyleSheet.d.ts +7 -0
  34. package/packages/core/dist/style/StyleSheet.d.ts.map +1 -0
  35. package/packages/core/dist/style/StyleSheet.js +13 -0
  36. package/packages/core/dist/style/StyleSheet.js.map +1 -0
  37. package/packages/core/dist/style/index.d.ts +3 -0
  38. package/packages/core/dist/style/index.d.ts.map +1 -0
  39. package/packages/core/dist/style/index.js +21 -0
  40. package/packages/core/dist/style/index.js.map +1 -0
  41. package/packages/core/src/index.ts +2 -1
  42. package/packages/core/src/style/StyleObject.ts +134 -0
  43. package/packages/core/src/style/StyleSheet.ts +19 -0
  44. package/packages/core/src/style/index.ts +5 -0
  45. package/packages/db/src/db.ts +4 -4
  46. package/packages/deskforge/src/index.ts +95 -0
  47. package/packages/file/dist/file.d.ts +1 -1
  48. package/packages/file/dist/file.d.ts.map +1 -1
  49. package/packages/file/dist/file.js +75 -3
  50. package/packages/file/dist/file.js.map +1 -1
  51. package/packages/file/src/file.ts +96 -4
  52. package/packages/file/test/file.test.js +23 -0
  53. package/packages/router/dist/index.d.ts +1 -0
  54. package/packages/router/dist/index.d.ts.map +1 -1
  55. package/packages/router/dist/index.js +1 -0
  56. package/packages/router/dist/index.js.map +1 -1
  57. package/packages/router/dist/navigation.d.ts +23 -0
  58. package/packages/router/dist/navigation.d.ts.map +1 -0
  59. package/packages/router/dist/navigation.js +61 -0
  60. package/packages/router/dist/navigation.js.map +1 -0
  61. package/packages/router/src/index.ts +2 -1
  62. package/packages/router/src/navigation.ts +86 -0
  63. package/packages/router/test/router.test.js +21 -1
  64. package/packages/theme/src/theme.ts +23 -0
  65. package/packages/ui/src/base/Button.ts +67 -0
  66. package/packages/ui/src/base/Image.ts +59 -0
  67. package/packages/ui/src/base/Pressable.ts +69 -0
  68. package/packages/ui/src/base/Text.ts +42 -0
  69. package/packages/ui/src/base/TextInput.ts +113 -0
  70. package/packages/ui/src/base/TouchableOpacity.ts +52 -0
  71. package/packages/ui/src/base/View.ts +43 -0
  72. package/packages/ui/src/index.ts +40 -25
  73. package/packages/ui/src/input/CheckBox.ts +69 -0
  74. package/packages/ui/src/input/Slider.ts +19 -2
  75. package/packages/ui/src/input/Switch.ts +68 -0
  76. package/packages/ui/src/layout/KeyboardAvoidingView.ts +65 -0
  77. package/packages/ui/src/layout/SafeAreaView.ts +58 -0
  78. package/packages/ui/src/list/FlatList.ts +111 -0
  79. package/packages/ui/src/list/ScrollView.ts +80 -0
  80. package/packages/ui/src/list/SectionList.ts +120 -0
  81. package/packages/ui/src/overlay/ActivityIndicator.ts +70 -0
  82. package/packages/ui/src/overlay/Modal.ts +28 -6
  83. package/packages/ui/src/platform/Platform.ts +119 -0
  84. package/packages/ui/src/platform/StatusBar.ts +63 -0
@@ -4,6 +4,9 @@
4
4
  // Read, write, move, copy, watch, pick — all in one clean API.
5
5
  // Works on desktop and mobile with platform-appropriate dialogs.
6
6
 
7
+ import * as fs from 'fs'
8
+ import * as path from 'path'
9
+
7
10
  // File types for the picker dialog
8
11
  type FileType =
9
12
  | 'any'
@@ -133,12 +136,101 @@ async function copy(
133
136
  console.log('[DeskForge File] Copy:', from, '→', to)
134
137
  }
135
138
 
136
- // Renames a file
139
+ // Renames a file and updates local imports that reference it
137
140
  async function rename(
138
- path: string,
139
- newName: string
141
+ oldPath: string,
142
+ newPath: string
140
143
  ): Promise<void> {
141
- console.log('[DeskForge File] Rename:', path, '→', newName)
144
+ const resolvedOldPath = path.resolve(oldPath)
145
+ const resolvedNewPath = path.resolve(newPath)
146
+
147
+ if (!fs.existsSync(resolvedOldPath)) {
148
+ throw new Error(`[DeskForge File] Cannot rename missing file: ${resolvedOldPath}`)
149
+ }
150
+
151
+ const parentDir = path.dirname(resolvedNewPath)
152
+ fs.mkdirSync(parentDir, { recursive: true })
153
+ fs.renameSync(resolvedOldPath, resolvedNewPath)
154
+
155
+ const workspaceRoot = path.dirname(resolvedOldPath)
156
+ const files = walkFiles(workspaceRoot)
157
+
158
+ for (const file of files) {
159
+ const content = fs.readFileSync(file, 'utf8')
160
+ const updated = rewriteImportReferences(content, file, resolvedOldPath, resolvedNewPath)
161
+
162
+ if (updated !== content) {
163
+ fs.writeFileSync(file, updated)
164
+ }
165
+ }
166
+
167
+ console.log('[DeskForge File] Rename:', oldPath, '→', newPath)
168
+ }
169
+
170
+ function walkFiles(root: string): string[] {
171
+ if (!fs.existsSync(root)) return []
172
+
173
+ const results: string[] = []
174
+ const stack = [root]
175
+
176
+ while (stack.length > 0) {
177
+ const current = stack.pop()
178
+ if (!current) continue
179
+
180
+ const entries = fs.readdirSync(current, { withFileTypes: true })
181
+ for (const entry of entries) {
182
+ const fullPath = path.join(current, entry.name)
183
+ if (entry.isDirectory()) {
184
+ stack.push(fullPath)
185
+ } else if (entry.isFile() && /\.(ts|tsx|js|jsx|json|md)$/i.test(entry.name)) {
186
+ results.push(fullPath)
187
+ }
188
+ }
189
+ }
190
+
191
+ return results
192
+ }
193
+
194
+ function toPosixPath(filePath: string): string {
195
+ return filePath.split(path.sep).join('/')
196
+ }
197
+
198
+ function rewriteImportReferences(
199
+ content: string,
200
+ filePath: string,
201
+ oldPath: string,
202
+ newPath: string
203
+ ): string {
204
+ const fileDir = path.dirname(filePath)
205
+ const oldBase = path.basename(oldPath)
206
+ const newBase = path.basename(newPath)
207
+ const oldBaseNoExt = path.basename(oldPath, path.extname(oldPath))
208
+ const newBaseNoExt = path.basename(newPath, path.extname(newPath))
209
+ const oldRelative = toPosixPath(path.relative(fileDir, oldPath))
210
+ const newRelative = toPosixPath(path.relative(fileDir, newPath))
211
+
212
+ const replacements = [
213
+ { from: oldRelative, to: newRelative },
214
+ { from: `./${oldBase}`, to: `./${newBase}` },
215
+ { from: `../${oldBase}`, to: `../${newBase}` },
216
+ { from: `./${oldBaseNoExt}`, to: `./${newBaseNoExt}` },
217
+ { from: `../${oldBaseNoExt}`, to: `../${newBaseNoExt}` },
218
+ { from: oldBase, to: newBase },
219
+ { from: oldBaseNoExt, to: newBaseNoExt },
220
+ ]
221
+
222
+ let updated = content
223
+ for (const { from, to } of replacements) {
224
+ if (from && from !== to) {
225
+ updated = updated.replace(new RegExp(escapeRegExp(from), 'g'), to)
226
+ }
227
+ }
228
+
229
+ return updated
230
+ }
231
+
232
+ function escapeRegExp(value: string): string {
233
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
142
234
  }
143
235
 
144
236
  // Deletes a file
@@ -0,0 +1,23 @@
1
+ const test = require('node:test')
2
+ const assert = require('node:assert/strict')
3
+ const fs = require('fs')
4
+ const os = require('os')
5
+ const path = require('path')
6
+
7
+ const { File } = require('../dist/file.js')
8
+
9
+ test('rename updates local import paths after a file rename', async () => {
10
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deskforge-file-rename-'))
11
+ const oldFile = path.join(tmpDir, 'oldName.ts')
12
+ const newFile = path.join(tmpDir, 'newName.ts')
13
+ const importer = path.join(tmpDir, 'app.ts')
14
+
15
+ fs.writeFileSync(oldFile, 'export const value = 1\n')
16
+ fs.writeFileSync(importer, "import { value } from './oldName'\nconsole.log(value)\n")
17
+
18
+ await File.rename(oldFile, newFile)
19
+
20
+ assert.ok(fs.existsSync(newFile))
21
+ assert.ok(!fs.existsSync(oldFile))
22
+ assert.match(fs.readFileSync(importer, 'utf8'), /newName/)
23
+ })
@@ -3,4 +3,5 @@ export * from './router';
3
3
  export * from './file-router';
4
4
  export * from './manifest';
5
5
  export * from './app';
6
+ export * from './navigation';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,OAAO,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,OAAO,CAAA;AACrB,cAAc,cAAc,CAAA"}
@@ -21,4 +21,5 @@ __exportStar(require("./router"), exports);
21
21
  __exportStar(require("./file-router"), exports);
22
22
  __exportStar(require("./manifest"), exports);
23
23
  __exportStar(require("./app"), exports);
24
+ __exportStar(require("./navigation"), exports);
24
25
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,WAAW;AACX,4CAA4C;AAC5C,2CAAwB;AACxB,2CAAwB;AACxB,gDAA6B;AAC7B,6CAA0B;AAC1B,wCAAqB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,WAAW;AACX,4CAA4C;AAC5C,2CAAwB;AACxB,2CAAwB;AACxB,gDAA6B;AAC7B,6CAA0B;AAC1B,wCAAqB;AACrB,+CAA4B"}
@@ -0,0 +1,23 @@
1
+ import type { DFNode } from './screen';
2
+ import type { ScreenComponent, ScreenConfig, ScreenParams } from './screen';
3
+ export interface NavigationStackEntry {
4
+ name: string;
5
+ params: ScreenParams;
6
+ }
7
+ export interface DeskForgeNavigation {
8
+ navigate: (name: string, params?: ScreenParams) => void;
9
+ goBack: () => void;
10
+ canGoBack: () => boolean;
11
+ currentRoute: () => string | null;
12
+ currentParams: () => ScreenParams;
13
+ getStack: () => NavigationStackEntry[];
14
+ render: () => DFNode | null;
15
+ register: (name: string, component: ScreenComponent | ScreenConfig) => void;
16
+ }
17
+ export declare function createNavigation(): DeskForgeNavigation;
18
+ export declare const navigation: DeskForgeNavigation;
19
+ export declare function createNavigationScreen(component: ScreenComponent, options?: {
20
+ title?: string;
21
+ layout?: string;
22
+ }): ScreenConfig;
23
+ //# sourceMappingURL=navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAG3E,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,YAAY,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,YAAY,KAAK,IAAI,CAAA;IACvD,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,SAAS,EAAE,MAAM,OAAO,CAAA;IACxB,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACjC,aAAa,EAAE,MAAM,YAAY,CAAA;IACjC,QAAQ,EAAE,MAAM,oBAAoB,EAAE,CAAA;IACtC,MAAM,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,GAAG,YAAY,KAAK,IAAI,CAAA;CAC5E;AAwDD,wBAAgB,gBAAgB,IAAI,mBAAmB,CAEtD;AAED,eAAO,MAAM,UAAU,qBAAqB,CAAA;AAE5C,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,eAAe,EAC1B,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAChD,YAAY,CAEd"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.navigation = void 0;
4
+ exports.createNavigation = createNavigation;
5
+ exports.createNavigationScreen = createNavigationScreen;
6
+ const screen_1 = require("./screen");
7
+ class DeskForgeNavigator {
8
+ constructor() {
9
+ this.routes = new Map();
10
+ this.stack = [];
11
+ this.index = -1;
12
+ }
13
+ navigate(name, params = {}) {
14
+ const route = this.routes.get(name);
15
+ if (!route) {
16
+ console.error(`[DeskForge Navigation] Unknown route: ${name}`);
17
+ return;
18
+ }
19
+ this.stack = this.stack.slice(0, this.index + 1);
20
+ this.stack.push({ name, params });
21
+ this.index = this.stack.length - 1;
22
+ }
23
+ goBack() {
24
+ if (this.index <= 0)
25
+ return;
26
+ this.index--;
27
+ }
28
+ canGoBack() {
29
+ return this.index > 0;
30
+ }
31
+ currentRoute() {
32
+ return this.stack[this.index]?.name ?? null;
33
+ }
34
+ currentParams() {
35
+ return this.stack[this.index]?.params ?? {};
36
+ }
37
+ getStack() {
38
+ return this.stack.map(entry => ({ ...entry, params: { ...entry.params } }));
39
+ }
40
+ render() {
41
+ const current = this.stack[this.index];
42
+ if (!current)
43
+ return null;
44
+ const route = this.routes.get(current.name);
45
+ if (!route)
46
+ return null;
47
+ const resolved = typeof route === 'function' ? route(current.params) : route.component(current.params);
48
+ return resolved ?? null;
49
+ }
50
+ register(name, component) {
51
+ this.routes.set(name, component);
52
+ }
53
+ }
54
+ function createNavigation() {
55
+ return new DeskForgeNavigator();
56
+ }
57
+ exports.navigation = createNavigation();
58
+ function createNavigationScreen(component, options = {}) {
59
+ return (0, screen_1.Screen)(component, options);
60
+ }
61
+ //# sourceMappingURL=navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":";;;AA0EA,4CAEC;AAID,wDAKC;AAnFD,qCAAiC;AAkBjC,MAAM,kBAAkB;IAAxB;QACU,WAAM,GAAG,IAAI,GAAG,EAA0C,CAAA;QAC1D,UAAK,GAA2B,EAAE,CAAA;QAClC,UAAK,GAAG,CAAC,CAAC,CAAA;IAiDpB,CAAC;IA/CC,QAAQ,CAAC,IAAY,EAAE,SAAuB,EAAE;QAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACnC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,yCAAyC,IAAI,EAAE,CAAC,CAAA;YAC9D,OAAM;QACR,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;QAChD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;QACjC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IACpC,CAAC;IAED,MAAM;QACJ,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC;YAAE,OAAM;QAC3B,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;IACvB,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,IAAI,CAAA;IAC7C,CAAC;IAED,aAAa;QACX,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,EAAE,CAAA;IAC7C,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAA;IAC7E,CAAC;IAED,MAAM;QACJ,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACtC,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAA;QAEzB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3C,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAA;QAEvB,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACtG,OAAO,QAAQ,IAAI,IAAI,CAAA;IACzB,CAAC;IAED,QAAQ,CAAC,IAAY,EAAE,SAAyC;QAC9D,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IAClC,CAAC;CACF;AAED,SAAgB,gBAAgB;IAC9B,OAAO,IAAI,kBAAkB,EAAE,CAAA;AACjC,CAAC;AAEY,QAAA,UAAU,GAAG,gBAAgB,EAAE,CAAA;AAE5C,SAAgB,sBAAsB,CACpC,SAA0B,EAC1B,UAA+C,EAAE;IAEjD,OAAO,IAAA,eAAM,EAAC,SAAS,EAAE,OAAO,CAAC,CAAA;AACnC,CAAC"}
@@ -4,4 +4,5 @@ export * from './screen'
4
4
  export * from './router'
5
5
  export * from './file-router'
6
6
  export * from './manifest'
7
- export * from './app'
7
+ export * from './app'
8
+ export * from './navigation'
@@ -0,0 +1,86 @@
1
+ import type { DFNode } from './screen'
2
+ import type { ScreenComponent, ScreenConfig, ScreenParams } from './screen'
3
+ import { Screen } from './screen'
4
+
5
+ export interface NavigationStackEntry {
6
+ name: string
7
+ params: ScreenParams
8
+ }
9
+
10
+ export interface DeskForgeNavigation {
11
+ navigate: (name: string, params?: ScreenParams) => void
12
+ goBack: () => void
13
+ canGoBack: () => boolean
14
+ currentRoute: () => string | null
15
+ currentParams: () => ScreenParams
16
+ getStack: () => NavigationStackEntry[]
17
+ render: () => DFNode | null
18
+ register: (name: string, component: ScreenComponent | ScreenConfig) => void
19
+ }
20
+
21
+ class DeskForgeNavigator implements DeskForgeNavigation {
22
+ private routes = new Map<string, ScreenComponent | ScreenConfig>()
23
+ private stack: NavigationStackEntry[] = []
24
+ private index = -1
25
+
26
+ navigate(name: string, params: ScreenParams = {}): void {
27
+ const route = this.routes.get(name)
28
+ if (!route) {
29
+ console.error(`[DeskForge Navigation] Unknown route: ${name}`)
30
+ return
31
+ }
32
+
33
+ this.stack = this.stack.slice(0, this.index + 1)
34
+ this.stack.push({ name, params })
35
+ this.index = this.stack.length - 1
36
+ }
37
+
38
+ goBack(): void {
39
+ if (this.index <= 0) return
40
+ this.index--
41
+ }
42
+
43
+ canGoBack(): boolean {
44
+ return this.index > 0
45
+ }
46
+
47
+ currentRoute(): string | null {
48
+ return this.stack[this.index]?.name ?? null
49
+ }
50
+
51
+ currentParams(): ScreenParams {
52
+ return this.stack[this.index]?.params ?? {}
53
+ }
54
+
55
+ getStack(): NavigationStackEntry[] {
56
+ return this.stack.map(entry => ({ ...entry, params: { ...entry.params } }))
57
+ }
58
+
59
+ render(): DFNode | null {
60
+ const current = this.stack[this.index]
61
+ if (!current) return null
62
+
63
+ const route = this.routes.get(current.name)
64
+ if (!route) return null
65
+
66
+ const resolved = typeof route === 'function' ? route(current.params) : route.component(current.params)
67
+ return resolved ?? null
68
+ }
69
+
70
+ register(name: string, component: ScreenComponent | ScreenConfig): void {
71
+ this.routes.set(name, component)
72
+ }
73
+ }
74
+
75
+ export function createNavigation(): DeskForgeNavigation {
76
+ return new DeskForgeNavigator()
77
+ }
78
+
79
+ export const navigation = createNavigation()
80
+
81
+ export function createNavigationScreen(
82
+ component: ScreenComponent,
83
+ options: { title?: string; layout?: string } = {}
84
+ ): ScreenConfig {
85
+ return Screen(component, options)
86
+ }
@@ -1,7 +1,7 @@
1
1
  const test = require('node:test')
2
2
  const assert = require('node:assert/strict')
3
3
 
4
- const { FileRouter, ManifestBuilder } = require('../dist/index.js')
4
+ const { FileRouter, ManifestBuilder, createNavigation } = require('../dist/index.js')
5
5
 
6
6
  function createNode() {
7
7
  return {
@@ -28,3 +28,23 @@ test('FileRouter resolves Expo-style routes and keeps route params', () => {
28
28
  assert.equal(router.getCurrentPath(), '/note/123')
29
29
  assert.deepEqual(router.render()?.props ?? {}, { title: '123' })
30
30
  })
31
+
32
+ test('createNavigation supports stack navigation and params', () => {
33
+ const navigation = createNavigation()
34
+
35
+ navigation.register('home', () => createNode())
36
+ navigation.register('details', (params = {}) => ({
37
+ ...createNode(),
38
+ props: { noteId: params.noteId ?? 'unknown' },
39
+ }))
40
+
41
+ navigation.navigate('home')
42
+ navigation.navigate('details', { noteId: '42' })
43
+
44
+ assert.equal(navigation.currentRoute(), 'details')
45
+ assert.deepEqual(navigation.currentParams(), { noteId: '42' })
46
+ assert.equal(navigation.render()?.props?.noteId, '42')
47
+ navigation.goBack()
48
+ assert.equal(navigation.currentRoute(), 'home')
49
+ assert.equal(navigation.canGoBack(), false)
50
+ })
@@ -133,8 +133,31 @@ function resolveToken(
133
133
  return current ?? path
134
134
  }
135
135
 
136
+ // Theme object — convenient namespace for theme operations
137
+ const Theme = {
138
+ setTheme,
139
+ getTokens,
140
+ getMode,
141
+ resolveToken,
142
+ onThemeChange,
143
+
144
+ /**
145
+ * Apply theme settings with convenient object syntax
146
+ * @example Theme.apply({ isDark: true })
147
+ * @example Theme.apply({ mode: 'light' })
148
+ */
149
+ apply(options: { isDark?: boolean; mode?: ThemeMode }): void {
150
+ if (options.isDark !== undefined) {
151
+ setTheme(options.isDark ? 'dark' : 'light')
152
+ } else if (options.mode !== undefined) {
153
+ setTheme(options.mode)
154
+ }
155
+ }
156
+ }
157
+
136
158
  export type { DFTheme, ThemeMode }
137
159
  export {
160
+ Theme,
138
161
  themeManager,
139
162
  setTheme,
140
163
  createTheme,
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Button Component
3
+ *
4
+ * Quick button shorthand component. Combines Text and Pressable for common button pattern.
5
+ * Accepts title, style, and handlers. Returns DFNode.
6
+ *
7
+ * No JSX. No HTML. No XML. Pure functional TypeScript.
8
+ */
9
+
10
+ import { createNode } from '@deskforge/core'
11
+ import type { DFNode, StyleObject } from '@deskforge/core'
12
+ import type { PressHandlers } from './Pressable'
13
+
14
+ /**
15
+ * Button - Quick button component
16
+ *
17
+ * Convenience component for creating buttons. Renders a Pressable with a Text child.
18
+ * Useful for common button patterns without manually nesting components.
19
+ *
20
+ * @param title - Button text label
21
+ * @param style - StyleObject containing layout and appearance properties
22
+ * @param handlers - PressHandlers object with onPress and other handlers
23
+ * @returns DFNode object with type 'Pressable' (button is a semantic shorthand)
24
+ *
25
+ * @example
26
+ * Button('Click Me', { backgroundColor: '#0077ff', padding: 12 }, {
27
+ * onPress: () => handleClick()
28
+ * })
29
+ *
30
+ * Button('Submit', {
31
+ * backgroundColor: '#2ecc71',
32
+ * paddingHorizontal: 20,
33
+ * paddingVertical: 10,
34
+ * borderRadius: 4,
35
+ * }, {
36
+ * onPress: () => submitForm()
37
+ * })
38
+ */
39
+ export function Button(
40
+ title: string,
41
+ style: StyleObject,
42
+ handlers: PressHandlers = {}
43
+ ): DFNode {
44
+ return createNode(
45
+ 'Pressable',
46
+ {
47
+ style,
48
+ onPress: handlers.onPress,
49
+ onLongPress: handlers.onLongPress,
50
+ onPressIn: handlers.onPressIn,
51
+ onPressOut: handlers.onPressOut,
52
+ disabled: handlers.disabled ?? false,
53
+ } as Record<string, unknown>,
54
+ [
55
+ createNode(
56
+ 'Text',
57
+ {
58
+ content: title,
59
+ style: {},
60
+ } as Record<string, unknown>,
61
+ []
62
+ ),
63
+ ]
64
+ )
65
+ }
66
+
67
+ export type { StyleObject, PressHandlers }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Image Component
3
+ *
4
+ * Displays images from file paths or URIs with optional styling.
5
+ * Accepts source and optional style object. Returns DFNode.
6
+ *
7
+ * No JSX. No HTML. No XML. Pure functional TypeScript.
8
+ */
9
+
10
+ import { createNode } from '@deskforge/core'
11
+ import type { DFNode, StyleObject } from '@deskforge/core'
12
+
13
+ /**
14
+ * Image source — can be string path or URI object
15
+ */
16
+ export type ImageSource = string | { uri: string }
17
+
18
+ /**
19
+ * Image - Image rendering component
20
+ *
21
+ * Renders an image from a file path or URI. Supports local files and remote URLs.
22
+ * Optional style object for sizing, positioning, and appearance.
23
+ *
24
+ * @param source - Image source as file path string or { uri: string } object
25
+ * @param style - Optional StyleObject for sizing and appearance
26
+ * @returns DFNode object with type 'Image'
27
+ *
28
+ * @example
29
+ * Image('/assets/logo.png', { width: 100, height: 100 })
30
+ *
31
+ * Image({ uri: 'https://example.com/image.png' }, {
32
+ * width: 200,
33
+ * height: 200,
34
+ * borderRadius: 12,
35
+ * })
36
+ *
37
+ * Image('/local/image.jpg', {
38
+ * width: '100%',
39
+ * height: 300,
40
+ * resizeMode: 'cover'
41
+ * })
42
+ */
43
+ export function Image(
44
+ source: ImageSource,
45
+ style?: StyleObject
46
+ ): DFNode {
47
+ const sourceObj = typeof source === 'string' ? { uri: source } : source
48
+
49
+ return createNode(
50
+ 'Image',
51
+ {
52
+ source: sourceObj,
53
+ style: style ?? {},
54
+ } as Record<string, unknown>,
55
+ []
56
+ )
57
+ }
58
+
59
+ export type { StyleObject }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Pressable Component
3
+ *
4
+ * Tappable container with press event handlers and animations.
5
+ * Accepts style, handlers object, and children array. Returns DFNode.
6
+ *
7
+ * No JSX. No HTML. No XML. Pure functional TypeScript.
8
+ */
9
+
10
+ import { createNode } from '@deskforge/core'
11
+ import type { DFNode, StyleObject } from '@deskforge/core'
12
+
13
+ /**
14
+ * Press handler configuration
15
+ *
16
+ * Defines all available press events for interactive components.
17
+ */
18
+ export interface PressHandlers {
19
+ /** Fired on tap or click */
20
+ onPress?: () => void
21
+ /** Fired after 500ms hold */
22
+ onLongPress?: () => void
23
+ /** Fired when press starts */
24
+ onPressIn?: () => void
25
+ /** Fired when press ends */
26
+ onPressOut?: () => void
27
+ /** Prevents all press events when true */
28
+ disabled?: boolean
29
+ }
30
+
31
+ /**
32
+ * Pressable - Tappable container component
33
+ *
34
+ * Renders a container that responds to press events. Automatically applies
35
+ * press animations and feedback. Children are rendered inside.
36
+ *
37
+ * @param style - StyleObject containing layout and appearance properties
38
+ * @param handlers - PressHandlers object with onPress, onLongPress, onPressIn, onPressOut, disabled
39
+ * @param children - Array of DFNode children to render inside the pressable
40
+ * @returns DFNode object with type 'Pressable'
41
+ *
42
+ * @example
43
+ * Pressable({ padding: 16, borderRadius: 8 }, {
44
+ * onPress: () => console.log('pressed'),
45
+ * onLongPress: () => console.log('long pressed'),
46
+ * }, [
47
+ * Text('Press me', { color: '#0077ff' })
48
+ * ])
49
+ */
50
+ export function Pressable(
51
+ style: StyleObject,
52
+ handlers: PressHandlers = {},
53
+ children: DFNode[] = []
54
+ ): DFNode {
55
+ return createNode(
56
+ 'Pressable',
57
+ {
58
+ style,
59
+ onPress: handlers.onPress,
60
+ onLongPress: handlers.onLongPress,
61
+ onPressIn: handlers.onPressIn,
62
+ onPressOut: handlers.onPressOut,
63
+ disabled: handlers.disabled ?? false,
64
+ } as Record<string, unknown>,
65
+ children
66
+ )
67
+ }
68
+
69
+ export type { StyleObject }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Text Component
3
+ *
4
+ * Renders text content with optional styling.
5
+ * Accepts content string or number and optional style object. Returns DFNode.
6
+ *
7
+ * No JSX. No HTML. No XML. Pure functional TypeScript.
8
+ */
9
+
10
+ import { createNode } from '@deskforge/core'
11
+ import type { DFNode, StyleObject } from '@deskforge/core'
12
+
13
+ /**
14
+ * Text - Text rendering component
15
+ *
16
+ * Renders text content with optional style object. Content can be string or number.
17
+ * All typography properties from React Native are supported via StyleObject.
18
+ *
19
+ * @param content - Text content to render (string or number)
20
+ * @param style - Optional StyleObject containing typography and appearance properties
21
+ * @returns DFNode object with type 'Text'
22
+ *
23
+ * @example
24
+ * Text('Hello World', { fontSize: 16, fontWeight: 'bold', color: '#000' })
25
+ * Text(42, { fontSize: 14 })
26
+ * Text('Simple', { color: '#666' })
27
+ */
28
+ export function Text(
29
+ content: string | number,
30
+ style?: StyleObject
31
+ ): DFNode {
32
+ return createNode(
33
+ 'Text',
34
+ {
35
+ content: String(content),
36
+ style: style ?? {},
37
+ } as Record<string, unknown>,
38
+ []
39
+ )
40
+ }
41
+
42
+ export type { StyleObject }