@umituz/web-landing 0.0.0-stage → 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.
package/README.md CHANGED
@@ -1,3 +1,36 @@
1
- # Temporary Holding Version
1
+ # @umituz/web-landing
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Public marketing-site shell for the umituz fleet — the public twin of
4
+ [`@umituz/web-dashboard`](https://www.npmjs.com/package/@umituz/web-dashboard).
5
+ Dashboard owns the authenticated app chrome; this package owns the landing
6
+ site chrome. Nothing public ever lives in the dashboard package.
7
+
8
+ ## What's inside
9
+
10
+ - `PublicSiteLayout` — skip link (first focusable element), header with
11
+ brand slot + NavLink nav (active-state class) + CTA pair (DS `Button`),
12
+ main landmark, footer brand/note/columns/legal, and scroll-restore on
13
+ navigation (owned here — apps don't duplicate it).
14
+ - `./site.css` — `ul-*` structural stylesheet on `@umituz/web-design-system`
15
+ semantic color variables. Import after the design-system stylesheet;
16
+ skin via your own `ul-site-*` overrides.
17
+
18
+ ## Usage
19
+
20
+ ```tsx
21
+ import { PublicSiteLayout } from '@umituz/web-landing/layouts/components';
22
+ import '@umituz/web-design-system/styles.css';
23
+ import '@umituz/web-landing/site.css';
24
+
25
+ <PublicSiteLayout
26
+ config={{
27
+ navItems: [{ to: '/features', label: 'Features' }],
28
+ brand: <Wordmark />,
29
+ ctaPrimary: { to: '/signup', label: 'Open the demo' },
30
+ footerColumns: [{ heading: 'Legal', links: [{ to: '/terms', label: 'Terms' }] }],
31
+ footerLegal: '© 2026',
32
+ }}
33
+ >
34
+ {children}
35
+ </PublicSiteLayout>
36
+ ```
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@umituz/web-landing",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Public marketing-site shell for the fleet — PublicSiteLayout (header nav, CTA pair, footer columns, skip link, scroll restore); ul-* stylesheet on design-system tokens. The public twin of @umituz/web-dashboard.",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "sideEffects": [
9
+ "*.css"
10
+ ],
11
+ "exports": {
12
+ ".": "./src/index.ts",
13
+ "./layouts": "./src/layouts/index.ts",
14
+ "./layouts/components": "./src/layouts/components/index.ts",
15
+ "./layouts/types": "./src/layouts/types/index.ts",
16
+ "./site.css": "./src/styles/site.css",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "src",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "typecheck": "tsc --noEmit",
25
+ "lint": "eslint src",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "version:patch": "npm version patch -m 'chore: release v%s'",
29
+ "version:minor": "npm version minor -m 'chore: release v%s'",
30
+ "version:major": "npm version major -m 'chore: release v%s'"
31
+ },
32
+ "peerDependencies": {
33
+ "react": "^18.0.0 || ^19.0.0",
34
+ "react-dom": "^18.0.0 || ^19.0.0",
35
+ "react-router-dom": "^6.0.0 || ^7.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@umituz/web-design-system": "^4.20.0",
39
+ "@umituz/web-localization": "^1.2.1"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.20.5",
43
+ "@types/react": "^19.2.14",
44
+ "@types/react-dom": "^19.2.3",
45
+ "@typescript-eslint/eslint-plugin": "^8.57.2",
46
+ "@typescript-eslint/parser": "^8.57.2",
47
+ "eslint": "^10.0.2",
48
+ "react-router-dom": "^7.13.1",
49
+ "typescript": "^5.9.3",
50
+ "vitest": "^5.0.1",
51
+ "@testing-library/dom": "^10.4.2",
52
+ "@testing-library/react": "^16.3.3",
53
+ "jsdom": "^29.1.1"
54
+ },
55
+ "keywords": [
56
+ "landing",
57
+ "marketing-site",
58
+ "public-shell",
59
+ "layout",
60
+ "react",
61
+ "typescript",
62
+ "package-driven"
63
+ ],
64
+ "author": "umituz",
65
+ "license": "MIT",
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "git+https://github.com/umituz/web-landing.git"
72
+ },
73
+ "bugs": {
74
+ "url": "https://github.com/umituz/web-landing/issues"
75
+ },
76
+ "homepage": "https://github.com/umituz/web-landing#readme"
77
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './layouts';
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { MemoryRouter, Routes, Route } from 'react-router-dom';
4
+ import { PublicSiteLayout } from './PublicSiteLayout';
5
+ import type { PublicLayoutConfig } from '../types/layout';
6
+
7
+ const config: PublicLayoutConfig = {
8
+ navItems: [
9
+ { to: '/features', label: 'Features' },
10
+ { to: '/pricing', label: 'Pricing' },
11
+ ],
12
+ brand: <a href="/">Brand</a>,
13
+ ctaPrimary: { to: '/signup', label: 'Open the demo' },
14
+ ctaSecondary: { to: '/signin', label: 'Sign in' },
15
+ footerBrand: <a href="/">Brand</a>,
16
+ footerNote: 'An independent workspace.',
17
+ footerColumns: [
18
+ { heading: 'Product', links: [{ to: '/features', label: 'Features' }, { to: '/pricing', label: 'Pricing' }] },
19
+ { heading: 'Legal', links: [{ to: '/terms', label: 'Terms' }] },
20
+ ],
21
+ footerLegal: '© 2026',
22
+ };
23
+
24
+ function renderAt(path: string) {
25
+ return render(
26
+ <MemoryRouter initialEntries={[path]}>
27
+ <Routes>
28
+ <Route path="*" element={<PublicSiteLayout config={config}>page-content</PublicSiteLayout>} />
29
+ </Routes>
30
+ </MemoryRouter>,
31
+ );
32
+ }
33
+
34
+ describe('PublicSiteLayout', () => {
35
+ it('renders a skip link targeting the main landmark', () => {
36
+ renderAt('/features');
37
+ expect(screen.getByRole('link', { name: /skip to content/i }).getAttribute('href')).toBe('#site-main');
38
+ expect(document.getElementById('site-main')).not.toBeNull();
39
+ });
40
+
41
+ it('marks the active nav route and leaves others plain', () => {
42
+ renderAt('/pricing');
43
+ const active = screen.getByRole('link', { name: 'Pricing', current: 'page' }) as HTMLElement;
44
+ expect(active.className).toContain('ul-site-link--active');
45
+ const idle = screen.getAllByRole('link', { name: 'Features' }).find((el) => el.className.includes('ul-site-link')) as HTMLElement;
46
+ expect(idle.className).not.toContain('ul-site-link--active');
47
+ });
48
+
49
+ it('renders CTA buttons, footer columns and the legal line', () => {
50
+ renderAt('/features');
51
+ expect(screen.getByRole('link', { name: 'Open the demo' })).toBeTruthy();
52
+ expect(screen.getByRole('link', { name: 'Sign in' })).toBeTruthy();
53
+ expect(screen.getByRole('contentinfo')).toBeTruthy();
54
+ expect(screen.getByRole('navigation', { name: 'Legal' })).toBeTruthy();
55
+ expect(screen.getByText('© 2026')).toBeTruthy();
56
+ });
57
+
58
+ it('renders children inside the main landmark', () => {
59
+ renderAt('/features');
60
+ expect(document.getElementById('site-main')?.textContent).toContain('page-content');
61
+ });
62
+ });
@@ -0,0 +1,86 @@
1
+ import { useEffect, type ReactNode } from 'react';
2
+ import { Link, NavLink, useLocation } from 'react-router-dom';
3
+ import { useT } from '@umituz/web-localization/react';
4
+ import { Button } from '@umituz/web-design-system/atoms';
5
+ import type { PublicLayoutConfig } from '../types/layout';
6
+
7
+ export interface PublicSiteLayoutProps {
8
+ config: PublicLayoutConfig;
9
+ children: ReactNode;
10
+ }
11
+
12
+ export const PublicSiteLayout = ({ config, children }: PublicSiteLayoutProps) => {
13
+ const location = useLocation();
14
+ const t = useT();
15
+
16
+ useEffect(() => {
17
+ window.scrollTo({ top: 0 });
18
+ }, [location.pathname]);
19
+
20
+ const mainId = config.mainId ?? 'site-main';
21
+
22
+ return (
23
+ <div className="ul-site">
24
+ <a href={`#${mainId}`} className="ul-skip-link">
25
+ {config.skipLabel ?? t('Skip to content')}
26
+ </a>
27
+
28
+ <header className="ul-site-header">
29
+ {config.brand && <div className="ul-site-brand">{config.brand}</div>}
30
+ <nav className="ul-site-nav" aria-label={config.navLabel ?? t('Site')}>
31
+ {config.navItems.map((item) => (
32
+ <NavLink
33
+ key={item.to}
34
+ to={item.to}
35
+ className={({ isActive }) => `ul-site-link ${isActive ? 'ul-site-link--active' : ''}`}
36
+ >
37
+ {item.label}
38
+ </NavLink>
39
+ ))}
40
+ </nav>
41
+ {(config.ctaSecondary || config.ctaPrimary) && (
42
+ <div className="ul-site-cta">
43
+ {config.ctaSecondary && (
44
+ <Button asChild variant="ghost" size="sm">
45
+ <Link to={config.ctaSecondary.to}>{config.ctaSecondary.label}</Link>
46
+ </Button>
47
+ )}
48
+ {config.ctaPrimary && (
49
+ <Button asChild variant="ink" size="sm">
50
+ <Link to={config.ctaPrimary.to}>{config.ctaPrimary.label}</Link>
51
+ </Button>
52
+ )}
53
+ </div>
54
+ )}
55
+ </header>
56
+
57
+ <main id={mainId} className="ul-site-main">
58
+ {children}
59
+ </main>
60
+
61
+ <footer className="ul-site-footer">
62
+ <div className="ul-site-footer-grid">
63
+ {(config.footerBrand || config.footerNote) && (
64
+ <div className="ul-site-footer-brand">
65
+ {config.footerBrand}
66
+ {config.footerNote && <div className="ul-site-note">{config.footerNote}</div>}
67
+ </div>
68
+ )}
69
+ {config.footerColumns.map((col) => (
70
+ <nav key={col.heading} className="ul-site-col" aria-label={col.heading}>
71
+ <h4>{col.heading}</h4>
72
+ {col.links.map((link) => (
73
+ <Link key={link.to} to={link.to}>
74
+ {link.label}
75
+ </Link>
76
+ ))}
77
+ </nav>
78
+ ))}
79
+ </div>
80
+ {config.footerLegal && <p className="ul-site-legal">{config.footerLegal}</p>}
81
+ </footer>
82
+ </div>
83
+ );
84
+ };
85
+
86
+ export default PublicSiteLayout;
@@ -0,0 +1,2 @@
1
+ export { PublicSiteLayout } from './PublicSiteLayout';
2
+ export type { PublicSiteLayoutProps } from './PublicSiteLayout';
@@ -0,0 +1,2 @@
1
+ export * from './components';
2
+ export * from './types';
@@ -0,0 +1 @@
1
+ export type { PublicLayoutConfig, PublicNavLink, PublicFooterColumn } from './layout';
@@ -0,0 +1,23 @@
1
+ export interface PublicNavLink {
2
+ to: string;
3
+ label: string;
4
+ }
5
+
6
+ export interface PublicFooterColumn {
7
+ heading: string;
8
+ links: PublicNavLink[];
9
+ }
10
+
11
+ export interface PublicLayoutConfig {
12
+ navItems: PublicNavLink[];
13
+ navLabel?: string;
14
+ brand?: React.ReactNode;
15
+ ctaPrimary?: PublicNavLink;
16
+ ctaSecondary?: PublicNavLink;
17
+ footerBrand?: React.ReactNode;
18
+ footerNote?: React.ReactNode;
19
+ footerColumns: PublicFooterColumn[];
20
+ footerLegal?: React.ReactNode;
21
+ mainId?: string;
22
+ skipLabel?: string;
23
+ }
@@ -0,0 +1,107 @@
1
+ .ul-site {
2
+ --ul-space-1: 4px;
3
+ --ul-space-2: 8px;
4
+ --ul-space-3: 12px;
5
+ --ul-space-6: 24px;
6
+ --ul-space-8: 32px;
7
+ --ul-space-10: 40px;
8
+
9
+ display: flex;
10
+ flex-direction: column;
11
+ min-height: 100vh;
12
+ }
13
+
14
+ .ul-skip-link {
15
+ position: fixed;
16
+ top: var(--ul-space-2);
17
+ left: var(--ul-space-2);
18
+ z-index: 100;
19
+ transform: translateY(-200%);
20
+ padding: var(--ul-space-2) var(--ul-space-6);
21
+ border-radius: var(--radius-md);
22
+ background: var(--color-primary);
23
+ color: var(--color-primary-foreground);
24
+ font-size: 0.875rem;
25
+ font-weight: 500;
26
+ text-decoration: none;
27
+ transition: transform 0.1s ease;
28
+ }
29
+ .ul-skip-link:focus {
30
+ transform: translateY(0);
31
+ }
32
+
33
+ .ul-site-header {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: var(--ul-space-6);
37
+ padding: var(--ul-space-3) var(--ul-space-6);
38
+ }
39
+ .ul-site-brand {
40
+ display: flex;
41
+ align-items: center;
42
+ }
43
+ .ul-site-nav {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: var(--ul-space-1);
47
+ flex: 1;
48
+ }
49
+ .ul-site-link {
50
+ padding: var(--ul-space-2) var(--ul-space-3);
51
+ border-radius: var(--radius-md);
52
+ text-decoration: none;
53
+ color: var(--color-muted-foreground);
54
+ }
55
+ .ul-site-link:hover,
56
+ .ul-site-link--active {
57
+ color: var(--color-foreground);
58
+ }
59
+ .ul-site-cta {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: var(--ul-space-2);
63
+ }
64
+
65
+ .ul-site-main {
66
+ flex: 1;
67
+ }
68
+
69
+ .ul-site-footer {
70
+ padding: var(--ul-space-10) var(--ul-space-6) var(--ul-space-6);
71
+ }
72
+ .ul-site-footer-grid {
73
+ display: grid;
74
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
75
+ gap: var(--ul-space-8);
76
+ }
77
+ .ul-site-footer-brand {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: var(--ul-space-3);
81
+ }
82
+ .ul-site-note {
83
+ font-size: 0.875rem;
84
+ color: var(--color-muted-foreground);
85
+ max-width: 32ch;
86
+ }
87
+ .ul-site-col {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: var(--ul-space-2);
91
+ }
92
+ .ul-site-col h4 {
93
+ margin: 0 0 var(--ul-space-2);
94
+ font-size: 0.875rem;
95
+ }
96
+ .ul-site-col a {
97
+ text-decoration: none;
98
+ color: var(--color-muted-foreground);
99
+ }
100
+ .ul-site-col a:hover {
101
+ color: var(--color-foreground);
102
+ }
103
+ .ul-site-legal {
104
+ margin: var(--ul-space-8) 0 0;
105
+ font-size: 0.75rem;
106
+ color: var(--color-muted-foreground);
107
+ }
@@ -0,0 +1,23 @@
1
+ import { afterEach, vi } from 'vitest';
2
+ import { cleanup } from '@testing-library/react';
3
+
4
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
5
+
6
+ if (typeof window !== 'undefined')
7
+ Object.defineProperty(window, 'matchMedia', {
8
+ writable: true,
9
+ value: (query: string): MediaQueryList =>
10
+ ({
11
+ matches: query.includes('prefers-reduced-motion'),
12
+ media: query,
13
+ onchange: null,
14
+ addListener: vi.fn(), removeListener: vi.fn(),
15
+ addEventListener: vi.fn(),
16
+ removeEventListener: vi.fn(),
17
+ dispatchEvent: vi.fn(),
18
+ }) as unknown as MediaQueryList,
19
+ });
20
+
21
+ afterEach(() => {
22
+ cleanup();
23
+ });