@stone-js/starters 0.8.3 → 0.8.5

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 (91) hide show
  1. package/basic-react-declarative/app/Application.tsx +22 -6
  2. package/basic-react-declarative/assets/css/index.css +163 -18
  3. package/basic-react-declarative/package.json +8 -5
  4. package/basic-react-declarative/public/logo.svg +16 -0
  5. package/basic-react-declarative/tests/Application.spec.ts +13 -4
  6. package/basic-react-imperative/app/Application.tsx +22 -6
  7. package/basic-react-imperative/assets/css/index.css +163 -18
  8. package/basic-react-imperative/package.json +8 -5
  9. package/basic-react-imperative/public/logo.svg +16 -0
  10. package/basic-react-imperative/tests/Application.spec.ts +15 -4
  11. package/basic-service-declarative/app/Application.ts +18 -3
  12. package/basic-service-declarative/package.json +7 -4
  13. package/basic-service-declarative/tests/Application.spec.ts +3 -2
  14. package/basic-service-imperative/app/Application.ts +18 -3
  15. package/basic-service-imperative/package.json +7 -4
  16. package/basic-service-imperative/tests/Application.spec.ts +3 -2
  17. package/continuum-showcase/LICENSE +21 -0
  18. package/continuum-showcase/app/Application.ts +45 -0
  19. package/continuum-showcase/app/layouts/MainLayout.tsx +13 -8
  20. package/continuum-showcase/app/pages/BlogPostPage.tsx +6 -3
  21. package/continuum-showcase/app/pages/HomePage.tsx +21 -8
  22. package/continuum-showcase/assets/css/index.css +198 -0
  23. package/continuum-showcase/assets/images/logo.svg +16 -0
  24. package/continuum-showcase/package.json +28 -10
  25. package/continuum-showcase/tests/HomePage.spec.tsx +41 -0
  26. package/continuum-showcase/tsconfig.json +33 -0
  27. package/continuum-showcase/vitest.config.ts +31 -0
  28. package/full-react-declarative/app/layout/AppLayout/AppLayout.tsx +1 -1
  29. package/full-react-declarative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
  30. package/full-react-declarative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
  31. package/full-react-declarative/app/middleware/AuthMiddleware.ts +1 -1
  32. package/full-react-declarative/app/pages/home/HomePage.tsx +22 -21
  33. package/full-react-declarative/assets/css/index.css +167 -0
  34. package/full-react-declarative/package.json +13 -10
  35. package/full-react-declarative/public/logo.svg +16 -0
  36. package/full-react-declarative/tests/AppLayout.spec.ts +51 -0
  37. package/full-react-declarative/tests/HomePage.spec.ts +38 -0
  38. package/full-react-imperative/app/components/LayoutHeader/LayoutHeader.tsx +1 -1
  39. package/full-react-imperative/app/components/PostWidget/PostWidget.tsx +1 -1
  40. package/full-react-imperative/app/components/PullToRefresh/PullToRefresh.css +10 -0
  41. package/full-react-imperative/app/components/PullToRefresh/PullToRefresh.tsx +66 -0
  42. package/full-react-imperative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
  43. package/full-react-imperative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
  44. package/full-react-imperative/app/middleware/AuthMiddleware.ts +1 -1
  45. package/full-react-imperative/app/pages/home/HomePage.tsx +32 -16
  46. package/full-react-imperative/assets/css/index.css +167 -0
  47. package/full-react-imperative/package.json +14 -12
  48. package/full-react-imperative/public/logo.svg +16 -0
  49. package/full-react-imperative/tests/HomePage.spec.ts +45 -0
  50. package/full-react-imperative/tests/LayoutHeader.spec.ts +39 -0
  51. package/full-service-declarative/app/handlers/WelcomeEventHandler.ts +47 -0
  52. package/full-service-declarative/app/middleware/AuthMiddleware.ts +1 -1
  53. package/full-service-declarative/package.json +12 -9
  54. package/full-service-declarative/tests/PostService.spec.ts +56 -0
  55. package/full-service-declarative/tests/WelcomeEventHandler.spec.ts +54 -0
  56. package/full-service-declarative/vitest.config.ts +1 -1
  57. package/full-service-imperative/app/handlers/WelcomeEventHandler.ts +51 -0
  58. package/full-service-imperative/app/middleware/AuthMiddleware.ts +1 -1
  59. package/full-service-imperative/package.json +12 -9
  60. package/full-service-imperative/tests/PostService.spec.ts +47 -0
  61. package/full-service-imperative/tests/WelcomeEventHandler.spec.ts +31 -0
  62. package/full-service-imperative/vitest.config.ts +1 -1
  63. package/package.json +1 -1
  64. package/standard-react-declarative/app/pages/welcome/WelcomePage.tsx +37 -4
  65. package/standard-react-declarative/assets/css/index.css +163 -18
  66. package/standard-react-declarative/package.json +11 -8
  67. package/standard-react-declarative/public/logo.svg +16 -0
  68. package/standard-react-declarative/tests/WelcomePage.spec.ts +71 -0
  69. package/standard-react-imperative/app/pages/welcome/WelcomePage.tsx +37 -4
  70. package/standard-react-imperative/assets/css/index.css +163 -18
  71. package/standard-react-imperative/package.json +11 -8
  72. package/standard-react-imperative/public/logo.svg +16 -0
  73. package/standard-react-imperative/tests/WelcomePage.spec.ts +53 -0
  74. package/standard-service-declarative/app/handlers/WelcomeEventHandler.ts +2 -2
  75. package/standard-service-declarative/app/services/WelcomeService.ts +28 -2
  76. package/standard-service-declarative/package.json +9 -6
  77. package/standard-service-declarative/tests/Application.spec.ts +100 -0
  78. package/standard-service-declarative/vitest.config.ts +1 -1
  79. package/standard-service-imperative/app/handlers/welcomeEventHandler.ts +2 -4
  80. package/standard-service-imperative/app/services/welcomeService.ts +28 -2
  81. package/standard-service-imperative/package.json +9 -6
  82. package/standard-service-imperative/tests/Application.spec.ts +84 -0
  83. package/standard-service-imperative/vitest.config.ts +1 -1
  84. package/basic-react-declarative/public/logo.png +0 -0
  85. package/basic-react-declarative/tests/__snapshots__/Application.spec.ts.snap +0 -3
  86. package/basic-react-imperative/public/logo.png +0 -0
  87. package/basic-react-imperative/tests/__snapshots__/Application.spec.ts.snap +0 -3
  88. package/full-react-declarative/assets/img/logo.png +0 -0
  89. package/full-react-declarative/public/logo.png +0 -0
  90. package/full-react-imperative/assets/img/logo.png +0 -0
  91. package/full-react-imperative/public/logo.png +0 -0
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+ "experimentalDecorators": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "isolatedModules": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+ "resolveJsonModule": true,
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noUncheckedSideEffectImports": true,
25
+
26
+ "types": ["node", "vite/client", "vitest/globals"],
27
+ },
28
+ "include": [
29
+ "app",
30
+ "tests",
31
+ "vitest.config.ts"
32
+ ]
33
+ }
@@ -0,0 +1,31 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import { defineConfig } from 'vitest/config'
3
+
4
+ export default defineConfig({
5
+ // Mirror the CLI's static-asset aliases (see stone.config.mjs) so component imports like
6
+ // `import logo from '@img/logo.svg'` also resolve under the test runner.
7
+ resolve: {
8
+ alias: {
9
+ '@img': fileURLToPath(new URL('./assets/images', import.meta.url)),
10
+ '@css': fileURLToPath(new URL('./assets/css', import.meta.url)),
11
+ '@assets': fileURLToPath(new URL('./assets', import.meta.url))
12
+ }
13
+ },
14
+ test: {
15
+ globals: true,
16
+ environment: 'node',
17
+ include: ['./tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
18
+ coverage: {
19
+ provider: 'v8',
20
+ include: ['app/**/*.ts', 'app/**/*.tsx'],
21
+ reporter: ['text', 'html'],
22
+ reportsDirectory: './coverage',
23
+ watermarks: {
24
+ statements: [60, 80],
25
+ functions: [60, 80],
26
+ branches: [60, 80],
27
+ lines: [60, 80]
28
+ }
29
+ }
30
+ }
31
+ })
@@ -48,7 +48,7 @@ export class AppLayout implements IPageLayout {
48
48
  <header>
49
49
  <p>
50
50
  <StoneLink to='/' className='logo'>
51
- <img src='/logo.png' alt='Stone.js Logo' />
51
+ <img src='/logo.svg' alt='Stone.js' width={96} height={96} />
52
52
  <span>Stone.js</span>
53
53
  </StoneLink>
54
54
  </p>
@@ -19,7 +19,7 @@ export class ErrorLayout implements IPageLayout {
19
19
  <header className='mt-64'>
20
20
  <div className='container'>
21
21
  <p className='pa-24'>
22
- <img src='/logo.png' className='logo' alt='Stone.js Logo' />
22
+ <img src='/logo.svg' className='logo' alt='Stone.js' />
23
23
  </p>
24
24
  </div>
25
25
  </header>
@@ -19,7 +19,7 @@ export class SecurityLayout implements IPageLayout {
19
19
  <header className='mt-64'>
20
20
  <div className='container'>
21
21
  <p className='pa-24'>
22
- <img src='/logo.png' className='logo' alt='Stone.js Logo' />
22
+ <img src='/logo.svg' className='logo' alt='Stone.js' />
23
23
  </p>
24
24
  </div>
25
25
  </header>
@@ -18,7 +18,7 @@ export interface AuthMiddlewareOptions {
18
18
  */
19
19
  @Middleware({
20
20
  global: true,
21
- params: [['/login', '/register']]
21
+ params: [['/', '/login', '/register']]
22
22
  })
23
23
  export class AuthMiddleware implements IMiddleware<ReactIncomingEvent, ReactOutgoingResponse> {
24
24
  private readonly snapshot: ISnapshot
@@ -8,8 +8,8 @@ import { IPage, Page, ReactIncomingEvent, StoneLink, HeadContext } from '@stone-
8
8
  export class HomePage implements IPage<ReactIncomingEvent> {
9
9
  head (): HeadContext {
10
10
  return {
11
- title: 'Home',
12
- description: 'Welcome to the Stone.js Blog Dashboard'
11
+ title: 'Welcome to Stone.js',
12
+ description: 'Stone.js Blog Dashboard — write your domain once, Stone.js is the context that runs it anywhere.'
13
13
  }
14
14
  }
15
15
 
@@ -20,25 +20,26 @@ export class HomePage implements IPage<ReactIncomingEvent> {
20
20
  */
21
21
  render (): JSX.Element {
22
22
  return (
23
- <div>
24
- <h1>Stone.js Blog Dashboard</h1>
25
- <p>Welcome to the Stone.js Blog Dashboard.</p>
26
- <div>
27
- <h2>Posts</h2>
28
- <p>View the latest posts from the Stone.js Blog.</p>
29
- <StoneLink to='/posts'>View Posts</StoneLink>
30
- </div>
31
- <div>
32
- <h2>Users</h2>
33
- <p>View the latest users from the Stone.js Blog.</p>
34
- <StoneLink to='/users'>View Users</StoneLink>
35
- </div>
36
- <div>
37
- <h2>Manage</h2>
38
- <p>Manage your profile</p>
39
- <StoneLink to='/manage'>Manage</StoneLink>
40
- </div>
41
- </div>
23
+ <main className='stone-welcome'>
24
+ <div className='glow' aria-hidden='true' />
25
+ <section className='hero'>
26
+ <img className='mark' src='/logo.svg' alt='Stone.js' width={104} height={104} />
27
+ <p className='eyebrow'>Welcome to</p>
28
+ <h1 className='title'>Stone.js</h1>
29
+ <p className='lead'>Your Blog Dashboard is running.</p>
30
+ <p className='tagline'>
31
+ Write your domain once, Stone.js is the context that runs it anywhere:
32
+ server, serverless, browser, CLI and the edge.
33
+ </p>
34
+ <nav className='links'>
35
+ <StoneLink to='/posts'>Browse Posts</StoneLink>
36
+ <StoneLink to='/users'>Browse Users</StoneLink>
37
+ <a href='https://stonejs.dev/docs' target='_blank' rel='noreferrer noopener'>Documentation</a>
38
+ <a href='https://github.com/stone-foundation/stone-js-framework' target='_blank' rel='noreferrer noopener'>GitHub</a>
39
+ </nav>
40
+ </section>
41
+ <footer className='brand'><span className='dot'>●</span> Stone.js — the continuum framework</footer>
42
+ </main>
42
43
  )
43
44
  }
44
45
  }
@@ -299,3 +299,170 @@ h1, h2, h3, h4, h5, h6 {
299
299
  background: #FEE2E2;
300
300
  border-left: 4px solid #EF4444;
301
301
  }
302
+
303
+ /**
304
+ * Stone.js brand welcome hero — "Obsidienne & Braise".
305
+ * Fully scoped under .stone-welcome so it never clashes with the app's styles.
306
+ * Theme-aware: dark by default, light under prefers-color-scheme: light.
307
+ */
308
+ :root {
309
+ --sw-ink: #14100E;
310
+ --sw-ink-2: #1E1815;
311
+ --sw-cream: #F4EFE6;
312
+ --sw-cream-dim: #B8AE9E;
313
+ --sw-ember-1: #FFC46B;
314
+ --sw-ember-2: #FF5A1F;
315
+ --sw-line: rgba(244, 239, 230, 0.10);
316
+ --sw-card: rgba(244, 239, 230, 0.04);
317
+ --sw-bg: radial-gradient(1200px 600px at 50% -10%, #241C17 0%, var(--sw-ink) 55%);
318
+ }
319
+
320
+ @media (prefers-color-scheme: light) {
321
+ :root {
322
+ --sw-ink: #F4EFE6;
323
+ --sw-ink-2: #ffffff;
324
+ --sw-cream: #1B1613;
325
+ --sw-cream-dim: #6B6154;
326
+ --sw-line: rgba(27, 22, 19, 0.10);
327
+ --sw-card: rgba(27, 22, 19, 0.03);
328
+ --sw-bg: radial-gradient(1200px 600px at 50% -10%, #FFFFFF 0%, #F1E9DC 60%);
329
+ }
330
+ }
331
+
332
+ .stone-welcome,
333
+ .stone-welcome * { box-sizing: border-box; }
334
+
335
+ .stone-welcome {
336
+ position: relative;
337
+ min-height: 100vh;
338
+ display: flex;
339
+ flex-direction: column;
340
+ align-items: center;
341
+ justify-content: center;
342
+ padding: 48px 24px;
343
+ color: var(--sw-cream);
344
+ background: var(--sw-bg);
345
+ font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
346
+ -webkit-font-smoothing: antialiased;
347
+ overflow: hidden;
348
+ }
349
+
350
+ /* Ember glow behind the mark. */
351
+ .stone-welcome .glow {
352
+ position: absolute;
353
+ top: 8%;
354
+ left: 50%;
355
+ width: 520px;
356
+ height: 520px;
357
+ transform: translateX(-50%);
358
+ background: radial-gradient(circle, rgba(255, 90, 31, 0.22) 0%, rgba(255, 90, 31, 0) 68%);
359
+ filter: blur(8px);
360
+ pointer-events: none;
361
+ z-index: 0;
362
+ }
363
+
364
+ .stone-welcome .hero {
365
+ position: relative;
366
+ z-index: 1;
367
+ width: 100%;
368
+ max-width: 680px;
369
+ text-align: center;
370
+ animation: sw-rise 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;
371
+ }
372
+
373
+ .stone-welcome .mark {
374
+ width: 104px;
375
+ height: 104px;
376
+ filter: drop-shadow(0 8px 28px rgba(255, 90, 31, 0.35));
377
+ }
378
+
379
+ .stone-welcome .eyebrow {
380
+ margin: 22px 0 0;
381
+ font-size: 12px;
382
+ font-weight: 600;
383
+ letter-spacing: 0.28em;
384
+ text-transform: uppercase;
385
+ color: var(--sw-cream-dim);
386
+ }
387
+
388
+ .stone-welcome .title {
389
+ margin: 8px 0 0;
390
+ font-size: clamp(44px, 8vw, 76px);
391
+ font-weight: 800;
392
+ letter-spacing: -0.03em;
393
+ line-height: 1.02;
394
+ background: linear-gradient(92deg, var(--sw-ember-1), var(--sw-ember-2));
395
+ -webkit-background-clip: text;
396
+ background-clip: text;
397
+ color: transparent;
398
+ }
399
+
400
+ .stone-welcome .lead {
401
+ margin: 18px auto 0;
402
+ font-size: 20px;
403
+ font-weight: 600;
404
+ color: var(--sw-cream);
405
+ }
406
+
407
+ .stone-welcome .tagline {
408
+ margin: 10px auto 0;
409
+ max-width: 46ch;
410
+ font-size: 16px;
411
+ line-height: 1.6;
412
+ color: var(--sw-cream-dim);
413
+ }
414
+
415
+ .stone-welcome .links {
416
+ margin-top: 34px;
417
+ display: flex;
418
+ flex-wrap: wrap;
419
+ gap: 12px;
420
+ justify-content: center;
421
+ }
422
+
423
+ .stone-welcome .links a,
424
+ .stone-welcome .links .edit {
425
+ display: inline-flex;
426
+ align-items: center;
427
+ gap: 8px;
428
+ padding: 11px 18px;
429
+ border: 1px solid var(--sw-line);
430
+ border-radius: 12px;
431
+ background: var(--sw-card);
432
+ color: var(--sw-cream);
433
+ font-size: 14px;
434
+ font-weight: 600;
435
+ text-decoration: none;
436
+ transition: border-color 0.2s, transform 0.2s, background 0.2s;
437
+ }
438
+
439
+ .stone-welcome .links a:hover {
440
+ transform: translateY(-2px);
441
+ border-color: rgba(255, 90, 31, 0.6);
442
+ background: rgba(255, 90, 31, 0.08);
443
+ }
444
+
445
+ .stone-welcome .links .edit {
446
+ font-family: ui-monospace, 'SF Mono', Menlo, Monaco, Consolas, monospace;
447
+ color: var(--sw-cream-dim);
448
+ }
449
+ .stone-welcome .links .edit b { color: var(--sw-ember-1); font-weight: 700; }
450
+
451
+ .stone-welcome .brand {
452
+ position: relative;
453
+ z-index: 1;
454
+ margin-top: 56px;
455
+ font-size: 13px;
456
+ letter-spacing: 0.02em;
457
+ color: var(--sw-cream-dim);
458
+ }
459
+ .stone-welcome .brand .dot { color: var(--sw-ember-2); }
460
+
461
+ @keyframes sw-rise {
462
+ from { opacity: 0; transform: translateY(14px); }
463
+ to { opacity: 1; transform: translateY(0); }
464
+ }
465
+
466
+ @media (prefers-reduced-motion: reduce) {
467
+ .stone-welcome .hero { animation: none; }
468
+ }
@@ -18,15 +18,15 @@
18
18
  "test:cvg": "npm run test -- --coverage"
19
19
  },
20
20
  "dependencies": {
21
- "@stone-js/env": "^0.8.3",
22
- "@stone-js/http-core": "^0.8.3",
23
- "@stone-js/node-http-adapter": "^0.8.3",
24
- "@stone-js/router": "^0.8.3",
25
- "@stone-js/filesystem": "^0.8.3",
26
- "@stone-js/core": "^0.8.3",
27
- "@stone-js/use-react": "^0.8.3",
28
- "@stone-js/browser-adapter": "^0.8.3",
29
- "@stone-js/node-cli-adapter": "^0.8.3",
21
+ "@stone-js/env": "^0.8.5",
22
+ "@stone-js/http-core": "^0.8.5",
23
+ "@stone-js/node-http-adapter": "^0.8.5",
24
+ "@stone-js/router": "^0.8.5",
25
+ "@stone-js/filesystem": "^0.8.5",
26
+ "@stone-js/core": "^0.8.5",
27
+ "@stone-js/use-react": "^0.8.5",
28
+ "@stone-js/browser-adapter": "^0.8.5",
29
+ "@stone-js/node-cli-adapter": "^0.8.5",
30
30
  "axios": "^1.7.9",
31
31
  "react": "^19.0.0",
32
32
  "react-dom": "^19.0.0"
@@ -35,12 +35,15 @@
35
35
  "@babel/plugin-proposal-decorators": "^7.25.9",
36
36
  "@babel/preset-env": "^7.26.9",
37
37
  "@babel/preset-typescript": "^7.27.0",
38
- "@stone-js/cli": "^0.8.3",
38
+ "@stone-js/cli": "^0.8.5",
39
39
  "@types/axios": "^0.14.4",
40
40
  "@types/node": "^24.0.7",
41
41
  "@types/react": "^19.0.7",
42
42
  "@types/react-dom": "^19.0.3",
43
43
  "@vitest/coverage-v8": "^3.0.9",
44
44
  "vitest": "^3.0.9"
45
+ },
46
+ "overrides": {
47
+ "glob": "^11.0.0"
45
48
  }
46
49
  }
@@ -0,0 +1,16 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Stone.js">
2
+ <title>Stone.js</title>
3
+ <style>
4
+ .ink { stroke: #1B1613; }
5
+ @media (prefers-color-scheme: dark) { .ink { stroke: #EFE8DC; } }
6
+ </style>
7
+ <defs>
8
+ <linearGradient id="braise" gradientUnits="userSpaceOnUse" x1="22" y1="14" x2="74" y2="26.1">
9
+ <stop offset="0" stop-color="#FFC46B" />
10
+ <stop offset="1" stop-color="#FF5A1F" />
11
+ </linearGradient>
12
+ </defs>
13
+ <path class="ink" d="M 79.96 36.4 A 34 34 0 0 1 53.9 81.5" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
14
+ <path class="ink" d="M 42.1 81.5 A 34 34 0 0 1 16.05 36.4" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
15
+ <path d="M 22 26.1 A 34 34 0 0 1 74 26.1" fill="none" stroke="url(#braise)" stroke-width="11" stroke-linecap="round" />
16
+ </svg>
@@ -0,0 +1,51 @@
1
+ import { renderToString } from 'react-dom/server'
2
+ import { AppLayout } from '../app/layout/AppLayout/AppLayout'
3
+
4
+ // We must mock decorators and router-bound components to lighten the test environment
5
+ vi.mock('@stone-js/use-react', async (importOriginal) => {
6
+ const actual: any = await importOriginal()
7
+ return {
8
+ ...actual,
9
+ PageLayout: vi.fn(() => vi.fn()),
10
+ StoneLink: ({ children }: any) => children,
11
+ StoneOutlet: ({ children }: any) => children
12
+ }
13
+ })
14
+
15
+ describe('AppLayout', () => {
16
+ let layout: AppLayout
17
+
18
+ beforeEach(() => {
19
+ layout = new AppLayout({} as any)
20
+ })
21
+
22
+ const render = (): string => {
23
+ const container = {
24
+ make: () => ({ getUser: () => undefined })
25
+ }
26
+ return renderToString(layout.render({ container, children: null } as any))
27
+ }
28
+
29
+ it('should create an App Layout instance', () => {
30
+ // Assert
31
+ expect(layout).toBeInstanceOf(AppLayout)
32
+ })
33
+
34
+ it('should render the real Stone.js logo', () => {
35
+ // Act
36
+ const html = render()
37
+
38
+ // Assert
39
+ expect(html).toContain('/logo.svg')
40
+ expect(html).toContain('alt="Stone.js"')
41
+ })
42
+
43
+ it('should render the unauthenticated header', () => {
44
+ // Act
45
+ const html = render()
46
+
47
+ // Assert
48
+ expect(html).toContain('Login')
49
+ expect(html).toContain('Stone.js')
50
+ })
51
+ })
@@ -0,0 +1,38 @@
1
+ import { renderToString } from 'react-dom/server'
2
+ import { HomePage } from '../app/pages/home/HomePage'
3
+
4
+ // We must mock the @Page decorator and router-bound components to lighten the test environment
5
+ vi.mock('@stone-js/use-react', async (importOriginal) => {
6
+ const actual: any = await importOriginal()
7
+ return {
8
+ ...actual,
9
+ Page: vi.fn(() => vi.fn()),
10
+ StoneLink: ({ children }: any) => children
11
+ }
12
+ })
13
+
14
+ describe('HomePage', () => {
15
+ const render = (): string => {
16
+ const page = new HomePage()
17
+ return renderToString(page.render())
18
+ }
19
+
20
+ it('should render the premium Stone.js welcome hero with the real logo', () => {
21
+ // Act
22
+ const html = render()
23
+
24
+ // Assert
25
+ expect(html).toContain('/logo.svg')
26
+ expect(html).toContain('alt="Stone.js"')
27
+ })
28
+
29
+ it('should render the Welcome to Stone.js headline', () => {
30
+ // Act
31
+ const html = render()
32
+
33
+ // Assert
34
+ expect(html).toContain('Welcome')
35
+ expect(html).toContain('Stone.js')
36
+ expect(html).toContain('stone-welcome')
37
+ })
38
+ })
@@ -27,7 +27,7 @@ export const LayoutHeader: FC<LayoutHeaderOptions> = ({ container }) => {
27
27
  <nav className='app-navbar'>
28
28
  <div className='header-left'>
29
29
  <StoneLink to='/' className='logo'>
30
- <img src='/logo.png' alt='Stone.js Logo' />
30
+ <img src='/logo.svg' alt='Stone.js' width={96} height={96} />
31
31
  <span>Stone.js</span>
32
32
  </StoneLink>
33
33
  </div>
@@ -2,9 +2,9 @@ import { User } from '../../models/User'
2
2
  import { Post } from '../../models/Post'
3
3
  import { deletePost } from '../../pages/utils'
4
4
  import { PostCard } from '../PostCard/PostCard'
5
- import PullToRefresh from 'react-pull-to-refresh'
6
5
  import { IContainer, ILogger } from '@stone-js/core'
7
6
  import { FC, useEffect, useRef, useState } from 'react'
7
+ import { PullToRefresh } from '../PullToRefresh/PullToRefresh'
8
8
  import { PostSkeleton } from '../PostSkeleton/PostSkeleton'
9
9
  import { CommentWidget } from '../CommentWidget/CommentWidget'
10
10
  import { IRouter, ReactIncomingEvent } from '@stone-js/use-react'
@@ -0,0 +1,10 @@
1
+ .pull-to-refresh {
2
+ will-change: transform;
3
+ }
4
+
5
+ .pull-to-refresh__indicator {
6
+ text-align: center;
7
+ padding: 8px 0;
8
+ font-size: 0.85rem;
9
+ opacity: 0.7;
10
+ }
@@ -0,0 +1,66 @@
1
+ import './PullToRefresh.css'
2
+ import { FC, ReactNode, TouchEvent, useRef, useState } from 'react'
3
+
4
+ /**
5
+ * PullToRefresh options.
6
+ */
7
+ export interface PullToRefreshOptions {
8
+ children: ReactNode
9
+ threshold?: number
10
+ onRefresh: () => Promise<void> | void
11
+ }
12
+
13
+ /**
14
+ * Lightweight, SSR-safe pull-to-refresh wrapper.
15
+ *
16
+ * Implemented in-app (with no dependency that touches `window` at import time)
17
+ * so it renders safely during server-side rendering and static route
18
+ * generation. The pull gesture is a progressive enhancement that only activates
19
+ * from touch interactions on the client.
20
+ */
21
+ export const PullToRefresh: FC<PullToRefreshOptions> = ({ children, onRefresh, threshold = 64 }) => {
22
+ const startY = useRef<number | null>(null)
23
+ const [pullY, setPullY] = useState(0)
24
+ const [refreshing, setRefreshing] = useState(false)
25
+
26
+ const onTouchStart = (event: TouchEvent): void => {
27
+ if (refreshing) { return }
28
+ // Only start a pull when the page is scrolled to the very top.
29
+ if (document.documentElement.scrollTop <= 0) {
30
+ startY.current = event.touches[0].clientY
31
+ }
32
+ }
33
+
34
+ const onTouchMove = (event: TouchEvent): void => {
35
+ if (startY.current === null || refreshing) { return }
36
+ const delta = event.touches[0].clientY - startY.current
37
+ if (delta > 0) { setPullY(Math.min(delta, threshold * 1.5)) }
38
+ }
39
+
40
+ const onTouchEnd = (): void => {
41
+ if (startY.current === null) { return }
42
+ const shouldRefresh = pullY >= threshold
43
+ startY.current = null
44
+ setPullY(0)
45
+ if (shouldRefresh) {
46
+ setRefreshing(true)
47
+ void Promise.resolve(onRefresh()).finally(() => setRefreshing(false))
48
+ }
49
+ }
50
+
51
+ return (
52
+ <div
53
+ className='pull-to-refresh'
54
+ onTouchStart={onTouchStart}
55
+ onTouchMove={onTouchMove}
56
+ onTouchEnd={onTouchEnd}
57
+ style={{
58
+ transform: pullY > 0 ? `translateY(${pullY}px)` : undefined,
59
+ transition: pullY > 0 ? 'none' : 'transform .2s ease'
60
+ }}
61
+ >
62
+ {refreshing && <div className='pull-to-refresh__indicator'>Refreshing…</div>}
63
+ {children}
64
+ </div>
65
+ )
66
+ }
@@ -18,7 +18,7 @@ export const ErrorLayout = (): IPageLayout => ({
18
18
  <header className='mt-64'>
19
19
  <div className='container'>
20
20
  <p className='pa-24'>
21
- <img src='/logo.png' className='logo' alt='Stone.js Logo' />
21
+ <img src='/logo.svg' className='logo' alt='Stone.js' />
22
22
  </p>
23
23
  </div>
24
24
  </header>
@@ -18,7 +18,7 @@ export const SecurityLayout = (): IPageLayout => ({
18
18
  <header className='mt-64'>
19
19
  <div className='container'>
20
20
  <p className='pa-24'>
21
- <img src='/logo.png' className='logo' alt='Stone.js Logo' />
21
+ <img src='/logo.svg' className='logo' alt='Stone.js' />
22
22
  </p>
23
23
  </div>
24
24
  </header>
@@ -52,5 +52,5 @@ export const AuthMiddleware = ({
52
52
  */
53
53
  export const AuthMiddlewareBlueprint = defineMiddleware(
54
54
  AuthMiddleware,
55
- { global: true, params: [['/login', '/register']], isFactory: true }
55
+ { global: true, params: [['/', '/login', '/register']], isFactory: true }
56
56
  )