@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.
- package/basic-react-declarative/app/Application.tsx +22 -6
- package/basic-react-declarative/assets/css/index.css +163 -18
- package/basic-react-declarative/package.json +8 -5
- package/basic-react-declarative/public/logo.svg +16 -0
- package/basic-react-declarative/tests/Application.spec.ts +13 -4
- package/basic-react-imperative/app/Application.tsx +22 -6
- package/basic-react-imperative/assets/css/index.css +163 -18
- package/basic-react-imperative/package.json +8 -5
- package/basic-react-imperative/public/logo.svg +16 -0
- package/basic-react-imperative/tests/Application.spec.ts +15 -4
- package/basic-service-declarative/app/Application.ts +18 -3
- package/basic-service-declarative/package.json +7 -4
- package/basic-service-declarative/tests/Application.spec.ts +3 -2
- package/basic-service-imperative/app/Application.ts +18 -3
- package/basic-service-imperative/package.json +7 -4
- package/basic-service-imperative/tests/Application.spec.ts +3 -2
- package/continuum-showcase/LICENSE +21 -0
- package/continuum-showcase/app/Application.ts +45 -0
- package/continuum-showcase/app/layouts/MainLayout.tsx +13 -8
- package/continuum-showcase/app/pages/BlogPostPage.tsx +6 -3
- package/continuum-showcase/app/pages/HomePage.tsx +21 -8
- package/continuum-showcase/assets/css/index.css +198 -0
- package/continuum-showcase/assets/images/logo.svg +16 -0
- package/continuum-showcase/package.json +28 -10
- package/continuum-showcase/tests/HomePage.spec.tsx +41 -0
- package/continuum-showcase/tsconfig.json +33 -0
- package/continuum-showcase/vitest.config.ts +31 -0
- package/full-react-declarative/app/layout/AppLayout/AppLayout.tsx +1 -1
- package/full-react-declarative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
- package/full-react-declarative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
- package/full-react-declarative/app/middleware/AuthMiddleware.ts +1 -1
- package/full-react-declarative/app/pages/home/HomePage.tsx +22 -21
- package/full-react-declarative/assets/css/index.css +167 -0
- package/full-react-declarative/package.json +13 -10
- package/full-react-declarative/public/logo.svg +16 -0
- package/full-react-declarative/tests/AppLayout.spec.ts +51 -0
- package/full-react-declarative/tests/HomePage.spec.ts +38 -0
- package/full-react-imperative/app/components/LayoutHeader/LayoutHeader.tsx +1 -1
- package/full-react-imperative/app/components/PostWidget/PostWidget.tsx +1 -1
- package/full-react-imperative/app/components/PullToRefresh/PullToRefresh.css +10 -0
- package/full-react-imperative/app/components/PullToRefresh/PullToRefresh.tsx +66 -0
- package/full-react-imperative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
- package/full-react-imperative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
- package/full-react-imperative/app/middleware/AuthMiddleware.ts +1 -1
- package/full-react-imperative/app/pages/home/HomePage.tsx +32 -16
- package/full-react-imperative/assets/css/index.css +167 -0
- package/full-react-imperative/package.json +14 -12
- package/full-react-imperative/public/logo.svg +16 -0
- package/full-react-imperative/tests/HomePage.spec.ts +45 -0
- package/full-react-imperative/tests/LayoutHeader.spec.ts +39 -0
- package/full-service-declarative/app/handlers/WelcomeEventHandler.ts +47 -0
- package/full-service-declarative/app/middleware/AuthMiddleware.ts +1 -1
- package/full-service-declarative/package.json +12 -9
- package/full-service-declarative/tests/PostService.spec.ts +56 -0
- package/full-service-declarative/tests/WelcomeEventHandler.spec.ts +54 -0
- package/full-service-declarative/vitest.config.ts +1 -1
- package/full-service-imperative/app/handlers/WelcomeEventHandler.ts +51 -0
- package/full-service-imperative/app/middleware/AuthMiddleware.ts +1 -1
- package/full-service-imperative/package.json +12 -9
- package/full-service-imperative/tests/PostService.spec.ts +47 -0
- package/full-service-imperative/tests/WelcomeEventHandler.spec.ts +31 -0
- package/full-service-imperative/vitest.config.ts +1 -1
- package/package.json +1 -1
- package/standard-react-declarative/app/pages/welcome/WelcomePage.tsx +37 -4
- package/standard-react-declarative/assets/css/index.css +163 -18
- package/standard-react-declarative/package.json +11 -8
- package/standard-react-declarative/public/logo.svg +16 -0
- package/standard-react-declarative/tests/WelcomePage.spec.ts +71 -0
- package/standard-react-imperative/app/pages/welcome/WelcomePage.tsx +37 -4
- package/standard-react-imperative/assets/css/index.css +163 -18
- package/standard-react-imperative/package.json +11 -8
- package/standard-react-imperative/public/logo.svg +16 -0
- package/standard-react-imperative/tests/WelcomePage.spec.ts +53 -0
- package/standard-service-declarative/app/handlers/WelcomeEventHandler.ts +2 -2
- package/standard-service-declarative/app/services/WelcomeService.ts +28 -2
- package/standard-service-declarative/package.json +9 -6
- package/standard-service-declarative/tests/Application.spec.ts +100 -0
- package/standard-service-declarative/vitest.config.ts +1 -1
- package/standard-service-imperative/app/handlers/welcomeEventHandler.ts +2 -4
- package/standard-service-imperative/app/services/welcomeService.ts +28 -2
- package/standard-service-imperative/package.json +9 -6
- package/standard-service-imperative/tests/Application.spec.ts +84 -0
- package/standard-service-imperative/vitest.config.ts +1 -1
- package/basic-react-declarative/public/logo.png +0 -0
- package/basic-react-declarative/tests/__snapshots__/Application.spec.ts.snap +0 -3
- package/basic-react-imperative/public/logo.png +0 -0
- package/basic-react-imperative/tests/__snapshots__/Application.spec.ts.snap +0 -3
- package/full-react-declarative/assets/img/logo.png +0 -0
- package/full-react-declarative/public/logo.png +0 -0
- package/full-react-imperative/assets/img/logo.png +0 -0
- 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.
|
|
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.
|
|
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.
|
|
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: '
|
|
12
|
-
description: '
|
|
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
|
-
<
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</
|
|
41
|
-
|
|
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.
|
|
22
|
-
"@stone-js/http-core": "^0.8.
|
|
23
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
24
|
-
"@stone-js/router": "^0.8.
|
|
25
|
-
"@stone-js/filesystem": "^0.8.
|
|
26
|
-
"@stone-js/core": "^0.8.
|
|
27
|
-
"@stone-js/use-react": "^0.8.
|
|
28
|
-
"@stone-js/browser-adapter": "^0.8.
|
|
29
|
-
"@stone-js/node-cli-adapter": "^0.8.
|
|
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.
|
|
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.
|
|
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,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.
|
|
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.
|
|
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
|
)
|