@bleedingdev/modern-js-create 3.2.0-ultramodern.102 → 3.2.0-ultramodern.104

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.
@@ -13,6 +13,10 @@ import { useTranslation } from 'react-i18next';
13
13
  const fallbackLanguage = 'en';
14
14
  const supportedLanguages = ['en', 'cs'] as const;
15
15
  type SupportedLanguage = (typeof supportedLanguages)[number];
16
+ const languageDirections: Record<SupportedLanguage, 'ltr'> = {
17
+ cs: 'ltr',
18
+ en: 'ltr',
19
+ };
16
20
 
17
21
  const isSupportedLanguage = (value: string): value is SupportedLanguage =>
18
22
  supportedLanguages.includes(value as SupportedLanguage);
@@ -52,6 +56,8 @@ const Index = () => {
52
56
  const { language } = useModernI18n();
53
57
  const location = useLocation();
54
58
  const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
59
+ const pageTitle = t('home.meta.title');
60
+ const pageDescription = t('home.meta.description');
55
61
  const canonicalPath = localizedPath(location.pathname, currentLanguage);
56
62
  const suffix = locationSuffix(location);
57
63
  const languageOptions = supportedLanguages.map((code) => ({
@@ -81,12 +87,9 @@ const Index = () => {
81
87
  {{/if}}
82
88
  return (
83
89
  <div className="container-box">
84
- <Helmet>
85
- <link
86
- rel="icon"
87
- type="image/x-icon"
88
- href="https://lf3-static.bytednsdoc.com/obj/eden-cn/uhbfnupenuhf/favicon.ico"
89
- />
90
+ <Helmet htmlAttributes={{ dir: languageDirections[currentLanguage], lang: currentLanguage }}>
91
+ <title>{pageTitle}</title>
92
+ <meta name="description" content={pageDescription} />
90
93
  <link rel="canonical" href={absoluteUrl(canonicalPath)} />
91
94
  {supportedLanguages.map((code) => (
92
95
  <link
@@ -102,7 +105,10 @@ const Index = () => {
102
105
  rel="alternate"
103
106
  />
104
107
  </Helmet>
105
- <main>
108
+ <a className="skip-link" href="#starter-main">
109
+ {t('home.skipLink')}
110
+ </a>
111
+ <header className="starter-header">
106
112
  <nav className="language-switcher" aria-label={t('home.language.switcher')}>
107
113
  {languageOptions.map((option) => (
108
114
  <a
@@ -114,24 +120,32 @@ const Index = () => {
114
120
  </a>
115
121
  ))}
116
122
  </nav>
117
- <div className="title">
118
- {t('home.title')}
123
+ </header>
124
+ <main id="starter-main" className="starter-main">
125
+ <section className="hero" aria-labelledby="starter-heading">
119
126
  <img
120
127
  alt={t('home.logoAlt')}
121
128
  className="logo"
122
- src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/modern-js-logo.svg"
129
+ height={96}
130
+ src="/assets/ultramodern-logo.svg"
131
+ width={96}
123
132
  />
124
- <p className="name">{t('home.name')}</p>
125
- </div>
126
- <p className="description{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
127
- {t('home.description.intro')}{' '}
128
- <code className="code">presetUltramodern(...)</code>{' '}
129
- {t('home.description.afterPreset')}{' '}
130
- <code className="code">modern.config.ts</code>{' '}
131
- {t('home.description.afterConfig')}{' '}
132
- <code className="code">pnpm run ultramodern:check</code>{' '}
133
- {t('home.description.end')}
134
- </p>
133
+ <div className="hero-copy">
134
+ <p className="name">{t('home.name')}</p>
135
+ <h1 id="starter-heading" className="title">
136
+ {t('home.title')}
137
+ </h1>
138
+ <p className="description{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
139
+ {t('home.description.intro')}{' '}
140
+ <code className="code">presetUltramodern(...)</code>{' '}
141
+ {t('home.description.afterPreset')}{' '}
142
+ <code className="code">modern.config.ts</code>{' '}
143
+ {t('home.description.afterConfig')}{' '}
144
+ <code className="code">pnpm run ultramodern:check</code>{' '}
145
+ {t('home.description.end')}
146
+ </p>
147
+ </div>
148
+ </section>
135
149
  {{#if useEffectBff}}
136
150
  <p className="description effect-message{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
137
151
  {t('home.bff.response')} <code className="code">{effectMessage}</code>
@@ -146,11 +160,7 @@ const Index = () => {
146
160
  >
147
161
  <h2>
148
162
  {t('home.cards.guide.title')}
149
- <img
150
- alt=""
151
- className="arrow-right"
152
- src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
153
- />
163
+ <span aria-hidden="true" className="arrow-right" />
154
164
  </h2>
155
165
  <p>{t('home.cards.guide.body')}</p>
156
166
  </a>
@@ -162,11 +172,7 @@ const Index = () => {
162
172
  >
163
173
  <h2>
164
174
  {t('home.cards.config.title')}
165
- <img
166
- alt=""
167
- className="arrow-right"
168
- src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
169
- />
175
+ <span aria-hidden="true" className="arrow-right" />
170
176
  </h2>
171
177
  <p>{t('home.cards.config.body')}</p>
172
178
  </a>
@@ -178,11 +184,7 @@ const Index = () => {
178
184
  >
179
185
  <h2>
180
186
  {t('home.cards.gates.title')}
181
- <img
182
- alt=""
183
- className="arrow-right"
184
- src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
185
- />
187
+ <span aria-hidden="true" className="arrow-right" />
186
188
  </h2>
187
189
  <p>{t('home.cards.gates.body')}</p>
188
190
  </a>
@@ -194,11 +196,7 @@ const Index = () => {
194
196
  >
195
197
  <h2>
196
198
  {t('home.cards.bff.title')}
197
- <img
198
- alt=""
199
- className="arrow-right"
200
- src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
201
- />
199
+ <span aria-hidden="true" className="arrow-right" />
202
200
  </h2>
203
201
  <p>{t('home.cards.bff.body')}</p>
204
202
  </a>
@@ -1,129 +1,266 @@
1
1
  {{#if enableTailwind}}@import 'tailwindcss';
2
2
 
3
- {{/if}}html,
3
+ {{/if}}:root {
4
+ color-scheme: light;
5
+ --starter-bg: #f3f6f4;
6
+ --starter-surface: #ffffff;
7
+ --starter-text: #14201b;
8
+ --starter-muted: #52625b;
9
+ --starter-border: #cfdad4;
10
+ --starter-accent: #087f5b;
11
+ --starter-accent-strong: #075f48;
12
+ --starter-focus: #0b7285;
13
+ }
14
+
15
+ html {
16
+ scroll-behavior: smooth;
17
+ }
18
+
4
19
  body {
5
- padding: 0;
6
20
  margin: 0;
7
21
  font-family:
8
- PingFang SC,
9
- Hiragino Sans GB,
10
- Microsoft YaHei,
22
+ Inter,
23
+ ui-sans-serif,
24
+ system-ui,
25
+ -apple-system,
26
+ BlinkMacSystemFont,
27
+ Segoe UI,
11
28
  Arial,
12
29
  sans-serif;
13
- background: linear-gradient(to bottom, transparent, #fff) #eceeef;
30
+ color: var(--starter-text);
31
+ background: linear-gradient(180deg, #ffffff 0%, var(--starter-bg) 100%);
14
32
  }
15
33
 
16
34
  p {
17
35
  margin: 0;
18
36
  }
19
37
 
20
- * {
38
+ .container-box,
39
+ .container-box *,
40
+ .container-box *::before,
41
+ .container-box *::after {
21
42
  -webkit-font-smoothing: antialiased;
22
43
  -moz-osx-font-smoothing: grayscale;
23
44
  box-sizing: border-box;
24
45
  }
25
46
 
26
47
  .container-box {
27
- min-height: 100vh;
28
- max-width: 100%;
48
+ min-block-size: 100dvh;
49
+ inline-size: 100%;
29
50
  display: flex;
30
51
  flex-direction: column;
31
- justify-content: center;
32
- align-items: center;
33
- padding-top: 10px;
52
+ padding: 1.25rem;
34
53
  }
35
54
 
36
- main {
37
- flex: 1;
55
+ .skip-link {
56
+ position: fixed;
57
+ inset-block-start: 1rem;
58
+ inset-inline-start: 1rem;
59
+ z-index: 1;
60
+ padding: 0.65rem 0.9rem;
61
+ color: #ffffff;
62
+ background: var(--starter-accent-strong);
63
+ border-radius: 6px;
64
+ transform: translateY(-150%);
65
+ transition: transform 0.15s ease;
66
+ }
67
+
68
+ .skip-link:focus-visible {
69
+ transform: translateY(0);
70
+ }
71
+
72
+ .starter-header {
73
+ inline-size: min(100%, 72rem);
74
+ margin-inline: auto;
75
+ }
76
+
77
+ .language-switcher {
38
78
  display: flex;
39
- flex-direction: column;
40
- justify-content: center;
79
+ flex-wrap: wrap;
80
+ gap: 0.5rem;
81
+ justify-content: flex-end;
82
+ }
83
+
84
+ .language-switcher a {
85
+ min-block-size: 2.25rem;
86
+ padding: 0.55rem 0.8rem;
87
+ color: var(--starter-muted);
88
+ text-decoration: none;
89
+ border: 1px solid transparent;
90
+ border-radius: 999px;
91
+ }
92
+
93
+ .language-switcher a:hover {
94
+ color: var(--starter-text);
95
+ border-color: var(--starter-border);
96
+ }
97
+
98
+ .language-switcher a[aria-current='page'] {
99
+ color: var(--starter-accent-strong);
100
+ background: rgb(8 127 91 / 10%);
101
+ border-color: rgb(8 127 91 / 24%);
102
+ }
103
+
104
+ .starter-main {
105
+ flex: 1;
106
+ inline-size: min(100%, 72rem);
107
+ margin-inline: auto;
108
+ padding-block: 4rem 2rem;
109
+ display: grid;
110
+ gap: 3rem;
111
+ align-content: center;
112
+ }
113
+
114
+ .hero {
115
+ display: grid;
116
+ grid-template-columns: auto minmax(0, 1fr);
117
+ gap: 1.5rem;
41
118
  align-items: center;
42
119
  }
43
120
 
44
121
  .title {
45
- display: flex;
46
- margin: 4rem 0 4rem;
47
- align-items: center;
48
- font-size: 4rem;
49
- font-weight: 600;
122
+ margin: 0.35rem 0 0;
123
+ font-size: 3.5rem;
124
+ line-height: 1;
125
+ font-weight: 750;
126
+ letter-spacing: 0;
127
+ text-wrap: balance;
50
128
  }
51
129
 
52
130
  .logo {
53
- width: 6rem;
54
- margin: 7px 0 0 1rem;
131
+ inline-size: 6rem;
132
+ block-size: 6rem;
133
+ border-radius: 1.25rem;
134
+ filter: drop-shadow(0 1rem 1.5rem rgb(17 24 39 / 18%));
55
135
  }
56
136
 
57
137
  .name {
58
- color: #4ecaff;
138
+ color: var(--starter-accent-strong);
139
+ font-size: 0.95rem;
140
+ font-weight: 700;
141
+ line-height: 1.4;
59
142
  }
60
143
 
61
144
  .description {
62
- text-align: center;
63
145
  line-height: 1.5;
64
- font-size: 1.3rem;
65
- color: #1b3a42;
66
- margin-bottom: 5rem;
146
+ font-size: 1.15rem;
147
+ color: var(--starter-muted);
148
+ max-inline-size: 54rem;
149
+ margin-block-start: 1rem;
150
+ text-wrap: pretty;
151
+ }
152
+
153
+ .effect-message {
154
+ padding: 1rem;
155
+ background: var(--starter-surface);
156
+ border: 1px solid var(--starter-border);
157
+ border-radius: 8px;
67
158
  }
68
159
 
69
160
  .code {
70
- background: #fafafa;
71
- border-radius: 12px;
72
- padding: 0.6rem 0.9rem;
73
- font-size: 1.05rem;
161
+ display: inline-block;
162
+ padding: 0.15rem 0.35rem;
163
+ color: var(--starter-accent-strong);
164
+ background: rgb(8 127 91 / 10%);
165
+ border-radius: 6px;
166
+ font-size: 0.95em;
74
167
  font-family:
168
+ ui-monospace,
169
+ SFMono-Regular,
75
170
  Menlo,
76
171
  Monaco,
77
- Lucida Console,
78
- Liberation Mono,
79
- DejaVu Sans Mono,
80
- Bitstream Vera Sans Mono,
81
- Courier New,
82
172
  monospace;
83
173
  }
84
174
 
85
175
  .container-box .grid {
86
- display: flex;
176
+ display: grid;
177
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
87
178
  align-items: center;
88
- justify-content: center;
89
- width: 1100px;
90
- margin-top: 3rem;
179
+ gap: 1rem;
91
180
  }
92
181
 
93
182
  .card {
94
- padding: 1.5rem;
95
183
  display: flex;
96
184
  flex-direction: column;
97
- justify-content: center;
98
- height: 100px;
185
+ gap: 0.75rem;
186
+ min-block-size: 9rem;
187
+ padding: 1.25rem;
99
188
  color: inherit;
100
189
  text-decoration: none;
101
- transition: 0.15s ease;
102
- width: 45%;
190
+ background: var(--starter-surface);
191
+ border: 1px solid var(--starter-border);
192
+ border-radius: 8px;
193
+ transition:
194
+ border-color 0.15s ease,
195
+ transform 0.15s ease;
103
196
  }
104
197
 
105
- .card:hover,
106
- .card:focus {
107
- transform: scale(1.05);
198
+ .card:hover {
199
+ border-color: rgb(8 127 91 / 45%);
200
+ transform: translateY(-0.125rem);
201
+ }
202
+
203
+ .skip-link:focus-visible,
204
+ .language-switcher a:focus-visible,
205
+ .card:focus-visible {
206
+ outline: 3px solid var(--starter-focus);
207
+ outline-offset: 3px;
108
208
  }
109
209
 
110
210
  .card h2 {
111
211
  display: flex;
112
212
  align-items: center;
113
- font-size: 1.5rem;
213
+ gap: 0.55rem;
214
+ font-size: 1.25rem;
215
+ line-height: 1.2;
114
216
  margin: 0;
115
217
  padding: 0;
116
218
  }
117
219
 
118
220
  .card p {
119
- opacity: 0.6;
221
+ opacity: 0.7;
120
222
  font-size: 0.9rem;
121
223
  line-height: 1.5;
122
- margin-top: 1rem;
123
224
  }
124
225
 
125
226
  .arrow-right {
126
- width: 1.3rem;
127
- margin-left: 0.5rem;
128
- margin-top: 3px;
227
+ flex: none;
228
+ inline-size: 0.65rem;
229
+ block-size: 0.65rem;
230
+ border-block-start: 2px solid currentColor;
231
+ border-inline-end: 2px solid currentColor;
232
+ transform: rotate(45deg);
233
+ }
234
+
235
+ @media (max-width: 44rem) {
236
+ .container-box {
237
+ padding: 1rem;
238
+ }
239
+
240
+ .starter-main {
241
+ padding-block: 2rem 1rem;
242
+ }
243
+
244
+ .hero {
245
+ grid-template-columns: 1fr;
246
+ }
247
+
248
+ .title {
249
+ font-size: 2.5rem;
250
+ }
251
+ }
252
+
253
+ @media (prefers-reduced-motion: reduce) {
254
+ html {
255
+ scroll-behavior: auto;
256
+ }
257
+
258
+ .skip-link,
259
+ .card {
260
+ transition: none;
261
+ }
262
+
263
+ .card:hover {
264
+ transform: none;
265
+ }
129
266
  }
@@ -25,6 +25,63 @@ describe('generated UltraModern contract', () => {
25
25
  {{/unless}}
26
26
  });
27
27
 
28
+ test('keeps UltraModern starter web correctness defaults', () => {
29
+ const routePage = readText('src/routes/[lang]/page.tsx');
30
+ const routeCss = readText('src/routes/index.css');
31
+ const modernConfig = readText('modern.config.ts');
32
+ const enLocale = readJson<{
33
+ home?: {
34
+ meta?: {
35
+ description?: string;
36
+ title?: string;
37
+ };
38
+ skipLink?: string;
39
+ };
40
+ }>('config/public/locales/en/translation.json');
41
+
42
+ expect(fs.existsSync(path.join(root, 'config/favicon.svg'))).toBe(true);
43
+ expect(
44
+ fs.existsSync(
45
+ path.join(root, 'config/public/assets/ultramodern-logo.svg'),
46
+ ),
47
+ ).toBe(true);
48
+ expect(routePage).not.toContain('lf3-static.bytednsdoc.com');
49
+ expect(routePage).toContain('<Helmet');
50
+ expect(routePage).toContain('htmlAttributes={{');
51
+ expect(routePage).toContain('dir: languageDirections[currentLanguage]');
52
+ expect(routePage).toContain('lang: currentLanguage');
53
+ expect(routePage).toContain('<title>{pageTitle}</title>');
54
+ expect(routePage).toContain(
55
+ '<meta name="description" content={pageDescription} />',
56
+ );
57
+ expect(routePage).toContain(
58
+ '<main id="starter-main" className="starter-main">',
59
+ );
60
+ expect(routePage).toContain('<h1 id="starter-heading" className="title">');
61
+ expect(routePage).toContain('src="/assets/ultramodern-logo.svg"');
62
+ expect(routePage).toContain('height={96}');
63
+ expect(routePage).toContain('width={96}');
64
+ expect(routePage).toContain(
65
+ '<span aria-hidden="true" className="arrow-right" />',
66
+ );
67
+ expect(routePage).not.toContain('<div className="title">');
68
+ expect(modernConfig).toContain(
69
+ 'width=device-width, initial-scale=1.0, viewport-fit=cover',
70
+ );
71
+ expect(modernConfig).not.toContain('user-scalable=no');
72
+ expect(modernConfig).not.toContain('maximum-scale');
73
+ expect(routeCss).toContain('min-block-size: 100dvh');
74
+ expect(routeCss).toContain(
75
+ 'grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr))',
76
+ );
77
+ expect(routeCss).toContain(':focus-visible');
78
+ expect(routeCss).toContain('@media (prefers-reduced-motion: reduce)');
79
+ expect(routeCss).not.toContain('width: 1100px');
80
+ expect(enLocale.home?.meta?.title).toBe('UltraModern.js Starter');
81
+ expect(enLocale.home?.meta?.description).toBeTruthy();
82
+ expect(enLocale.home?.skipLink).toBeTruthy();
83
+ });
84
+
28
85
  test('retains package-source metadata for generated Modern.js packages', () => {
29
86
  const packageJson = readJson<{
30
87
  dependencies?: Record<string, string>;
@@ -32,6 +89,7 @@ describe('generated UltraModern contract', () => {
32
89
  modernjs?: {
33
90
  packageSource?: {
34
91
  config?: string;
92
+ strategy?: string;
35
93
  };
36
94
  preset?: string;
37
95
  };
@@ -48,15 +106,20 @@ describe('generated UltraModern contract', () => {
48
106
  expect(packageJson.modernjs?.packageSource?.config).toBe(
49
107
  './.modernjs/ultramodern-package-source.json',
50
108
  );
51
- expect(packageSource.strategy).toMatch(/^(workspace|install)$/u);
52
- expect(packageSource.modernPackages?.packages).toContain(
53
- '@modern-js/runtime',
54
- );
55
- expect(packageSource.modernPackages?.packages).toContain(
56
- '@modern-js/app-tools',
109
+ expect(packageJson.modernjs?.packageSource?.strategy).toBe(
110
+ packageSource.strategy,
57
111
  );
58
- expect(packageSource.modernPackages?.packages).toContain(
59
- '@modern-js/adapter-rstest',
112
+ expect(packageSource.strategy).toMatch(/^(workspace|install)$/u);
113
+ const generatedModernDependencies = [
114
+ ...new Set(
115
+ (['dependencies', 'devDependencies'] as const).flatMap(section =>
116
+ Object.keys(packageJson[section] ?? {}),
117
+ ),
118
+ ),
119
+ ].filter(packageName => packageName.startsWith('@modern-js/'));
120
+ expect(packageSource.modernPackages?.packages?.length).toBeGreaterThan(0);
121
+ expect(packageSource.modernPackages?.packages).toEqual(
122
+ expect.arrayContaining(generatedModernDependencies),
60
123
  );
61
124
  expect(packageSource.modernPackages?.specifier).toBeTruthy();
62
125
  expect(
@@ -116,6 +116,6 @@
116
116
  }
117
117
  ]
118
118
  },
119
- "include": ["src", "api", "shared", "config", "modern.config.ts"],
119
+ "include": ["src", "api", "shared", "config", "modern.config.ts", "server"],
120
120
  "exclude": ["**/node_modules"]
121
121
  }
@@ -19,9 +19,18 @@ pnpm dlx @bleedingdev/modern-js-create payments --vertical
19
19
  ```
20
20
 
21
21
  Each added vertical owns its UI/routes, browser-safe Module Federation exposes,
22
- localized route metadata, CSS prefix, Effect BFF handlers, local API contract,
23
- and typed client surface. Server handlers and Effect client/contract modules
24
- stay out of browser exposes.
22
+ private-first route metadata, localized URLs, public-route opt-ins, CSS prefix,
23
+ Effect BFF handlers, local API contract, and typed client surface. Server
24
+ handlers and Effect client/contract modules stay out of browser exposes.
25
+
26
+ ## Private-First Public Surfaces
27
+
28
+ Generated app routes are private and non-indexable by default. Private app,
29
+ auth, tenant, dashboard, and internal routes publish no discovery output unless
30
+ route metadata explicitly marks them `public && indexable`. The default scaffold
31
+ therefore emits only a disallowing `robots.txt`; sitemap, web manifest,
32
+ `llms.txt`, API catalog, security.txt, and JSON-LD output stay omitted until a
33
+ safe public route or public docs/help/product surface exists.
25
34
 
26
35
  Run the scaffold validator before adding business code and after every
27
36
  `--vertical` mutation: