openuispec 0.1.45 → 0.1.46
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 +2 -1
- package/cli/init.ts +5 -2
- package/examples/social-app/.mcp.json +10 -0
- package/examples/social-app/AGENTS.md +105 -0
- package/examples/social-app/CLAUDE.md +105 -0
- package/examples/social-app/README.md +19 -0
- package/examples/social-app/backend/.gitkeep +1 -0
- package/examples/social-app/generated/android/social-app/app/build.gradle.kts +92 -0
- package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +26 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +20 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +35 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +13 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +98 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +19 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +68 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +15 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +34 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +390 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +234 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +641 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +113 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +212 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +113 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +137 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +180 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +157 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +85 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +74 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +293 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +116 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +161 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +162 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +95 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +123 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +33 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +41 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +20 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +82 -0
- package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +60 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +9 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +91 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +10 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +79 -0
- package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +79 -0
- package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +23 -0
- package/examples/social-app/generated/android/social-app/build.gradle.kts +6 -0
- package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +48 -0
- package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/examples/social-app/generated/android/social-app/gradle.properties +11 -0
- package/examples/social-app/generated/android/social-app/gradlew +25 -0
- package/examples/social-app/generated/android/social-app/settings.gradle.kts +23 -0
- package/examples/social-app/generated/web/social-app/index.html +12 -0
- package/examples/social-app/generated/web/social-app/package-lock.json +2517 -0
- package/examples/social-app/generated/web/social-app/package.json +27 -0
- package/examples/social-app/generated/web/social-app/src/app/App.tsx +58 -0
- package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +247 -0
- package/examples/social-app/generated/web/social-app/src/components/cards.tsx +317 -0
- package/examples/social-app/generated/web/social-app/src/components/ui.tsx +328 -0
- package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +86 -0
- package/examples/social-app/generated/web/social-app/src/i18n.tsx +59 -0
- package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +85 -0
- package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +70 -0
- package/examples/social-app/generated/web/social-app/src/lib/utils.ts +97 -0
- package/examples/social-app/generated/web/social-app/src/locales/en.json +67 -0
- package/examples/social-app/generated/web/social-app/src/locales/ru.json +67 -0
- package/examples/social-app/generated/web/social-app/src/locales/uz.json +67 -0
- package/examples/social-app/generated/web/social-app/src/main.tsx +16 -0
- package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +90 -0
- package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +86 -0
- package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +57 -0
- package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +113 -0
- package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +52 -0
- package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +41 -0
- package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +115 -0
- package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +57 -0
- package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +76 -0
- package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +96 -0
- package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +77 -0
- package/examples/social-app/generated/web/social-app/src/state/store.ts +592 -0
- package/examples/social-app/generated/web/social-app/src/styles.css +124 -0
- package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +1 -0
- package/examples/social-app/generated/web/social-app/tsconfig.json +22 -0
- package/examples/social-app/generated/web/social-app/tsconfig.node.json +13 -0
- package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +1 -0
- package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +1 -0
- package/examples/social-app/generated/web/social-app/vite.config.d.ts +2 -0
- package/examples/social-app/generated/web/social-app/vite.config.js +6 -0
- package/examples/social-app/generated/web/social-app/vite.config.ts +7 -0
- package/examples/social-app/openuispec/README.md +56 -0
- package/examples/social-app/openuispec/contracts/.gitkeep +0 -0
- package/examples/social-app/openuispec/contracts/action_trigger.yaml +73 -0
- package/examples/social-app/openuispec/contracts/collection.yaml +43 -0
- package/examples/social-app/openuispec/contracts/data_display.yaml +47 -0
- package/examples/social-app/openuispec/contracts/feedback.yaml +49 -0
- package/examples/social-app/openuispec/contracts/input_field.yaml +41 -0
- package/examples/social-app/openuispec/contracts/nav_container.yaml +34 -0
- package/examples/social-app/openuispec/contracts/surface.yaml +41 -0
- package/examples/social-app/openuispec/flows/.gitkeep +0 -0
- package/examples/social-app/openuispec/flows/create_post.yaml +66 -0
- package/examples/social-app/openuispec/locales/.gitkeep +0 -0
- package/examples/social-app/openuispec/locales/en.json +67 -0
- package/examples/social-app/openuispec/locales/ru.json +67 -0
- package/examples/social-app/openuispec/locales/uz.json +67 -0
- package/examples/social-app/openuispec/openuispec.yaml +214 -0
- package/examples/social-app/openuispec/platform/.gitkeep +0 -0
- package/examples/social-app/openuispec/platform/android.yaml +30 -0
- package/examples/social-app/openuispec/platform/ios.yaml +19 -0
- package/examples/social-app/openuispec/platform/web.yaml +23 -0
- package/examples/social-app/openuispec/screens/.gitkeep +0 -0
- package/examples/social-app/openuispec/screens/chat_detail.yaml +53 -0
- package/examples/social-app/openuispec/screens/discover.yaml +78 -0
- package/examples/social-app/openuispec/screens/edit_profile.yaml +78 -0
- package/examples/social-app/openuispec/screens/home_feed.yaml +123 -0
- package/examples/social-app/openuispec/screens/messages_inbox.yaml +43 -0
- package/examples/social-app/openuispec/screens/notifications.yaml +29 -0
- package/examples/social-app/openuispec/screens/post_detail.yaml +86 -0
- package/examples/social-app/openuispec/screens/profile_self.yaml +53 -0
- package/examples/social-app/openuispec/screens/profile_user.yaml +60 -0
- package/examples/social-app/openuispec/screens/search_results.yaml +62 -0
- package/examples/social-app/openuispec/screens/settings.yaml +94 -0
- package/examples/social-app/openuispec/tokens/.gitkeep +0 -0
- package/examples/social-app/openuispec/tokens/color.yaml +76 -0
- package/examples/social-app/openuispec/tokens/elevation.yaml +31 -0
- package/examples/social-app/openuispec/tokens/icons.yaml +147 -0
- package/examples/social-app/openuispec/tokens/layout.yaml +37 -0
- package/examples/social-app/openuispec/tokens/motion.yaml +28 -0
- package/examples/social-app/openuispec/tokens/spacing.yaml +19 -0
- package/examples/social-app/openuispec/tokens/themes.yaml +31 -0
- package/examples/social-app/openuispec/tokens/typography.yaml +50 -0
- package/examples/social-app/package.json +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { type ButtonHTMLAttributes, type PropsWithChildren, type ReactNode } from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import { Icon } from "../lib/icons";
|
|
4
|
+
import { cn, getInitials } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
export function ScreenScaffold({
|
|
7
|
+
title,
|
|
8
|
+
subtitle,
|
|
9
|
+
children,
|
|
10
|
+
}: PropsWithChildren<{ title: string; subtitle?: string }>) {
|
|
11
|
+
return (
|
|
12
|
+
<section className="mx-auto flex w-full max-w-[860px] flex-col gap-6 px-4 pb-28 pt-4 md:px-6 xl:px-8">
|
|
13
|
+
<header className="space-y-2">
|
|
14
|
+
<p className="text-xs uppercase tracking-[0.28em] text-[var(--color-text-tertiary)]">social-app</p>
|
|
15
|
+
<h1 className="text-[clamp(1.5rem,3vw,2rem)] font-semibold leading-[1.2] text-[var(--color-text-primary)]">
|
|
16
|
+
{title}
|
|
17
|
+
</h1>
|
|
18
|
+
{subtitle ? <p className="max-w-2xl text-sm text-[var(--color-text-secondary)]">{subtitle}</p> : null}
|
|
19
|
+
</header>
|
|
20
|
+
{children}
|
|
21
|
+
</section>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function SectionTitle({ children, action }: { children: ReactNode; action?: ReactNode }) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex items-center justify-between gap-4">
|
|
28
|
+
<h2 className="text-lg font-semibold text-[var(--color-text-primary)]">{children}</h2>
|
|
29
|
+
{action}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Surface({
|
|
35
|
+
children,
|
|
36
|
+
className,
|
|
37
|
+
}: PropsWithChildren<{
|
|
38
|
+
className?: string;
|
|
39
|
+
}>) {
|
|
40
|
+
return <div className={cn("rounded-surface border border-[var(--color-border-default)] bg-[var(--color-surface-secondary)] shadow-sm", className)}>{children}</div>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ActionButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
44
|
+
variant?: "primary" | "secondary" | "chip" | "destructive";
|
|
45
|
+
icon?: Parameters<typeof Icon>[0]["name"];
|
|
46
|
+
fullWidth?: boolean;
|
|
47
|
+
selected?: boolean;
|
|
48
|
+
trailing?: ReactNode;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function ActionButton({
|
|
52
|
+
children,
|
|
53
|
+
className,
|
|
54
|
+
variant = "primary",
|
|
55
|
+
icon,
|
|
56
|
+
fullWidth,
|
|
57
|
+
selected,
|
|
58
|
+
trailing,
|
|
59
|
+
...props
|
|
60
|
+
}: ActionButtonProps) {
|
|
61
|
+
const variantClass =
|
|
62
|
+
variant === "primary"
|
|
63
|
+
? "rounded-cap-primary border-transparent bg-[var(--color-brand-primary)] text-[var(--color-brand-primary-on)]"
|
|
64
|
+
: variant === "secondary"
|
|
65
|
+
? "rounded-cap-alternate border-[var(--color-border-strong)] bg-transparent text-[var(--color-text-primary)]"
|
|
66
|
+
: variant === "destructive"
|
|
67
|
+
? "rounded-cap-primary border-transparent bg-[var(--color-semantic-danger)] text-white"
|
|
68
|
+
: selected
|
|
69
|
+
? "rounded-cap-primary border-transparent bg-[var(--color-brand-accent)] text-[var(--color-brand-accent-on)]"
|
|
70
|
+
: "rounded-cap-primary border-[var(--color-border-default)] bg-transparent text-[var(--color-text-primary)]";
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<button
|
|
74
|
+
className={cn(
|
|
75
|
+
"interactive-press inline-flex min-h-11 items-center justify-center gap-2 border px-4 py-3 text-sm font-semibold transition",
|
|
76
|
+
fullWidth && "w-full",
|
|
77
|
+
variantClass,
|
|
78
|
+
className,
|
|
79
|
+
)}
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
{icon ? <Icon name={icon} className="h-5 w-5" /> : null}
|
|
83
|
+
<span>{children}</span>
|
|
84
|
+
{trailing}
|
|
85
|
+
</button>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type TextFieldProps = {
|
|
90
|
+
label: string;
|
|
91
|
+
value: string;
|
|
92
|
+
multiline?: boolean;
|
|
93
|
+
onValueChange?: (value: string) => void;
|
|
94
|
+
trailingAction?: ReactNode;
|
|
95
|
+
placeholder?: string;
|
|
96
|
+
maxLength?: number;
|
|
97
|
+
type?: string;
|
|
98
|
+
className?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function TextField({
|
|
102
|
+
label,
|
|
103
|
+
value,
|
|
104
|
+
multiline,
|
|
105
|
+
className,
|
|
106
|
+
onValueChange,
|
|
107
|
+
trailingAction,
|
|
108
|
+
placeholder,
|
|
109
|
+
maxLength,
|
|
110
|
+
type,
|
|
111
|
+
}: TextFieldProps) {
|
|
112
|
+
const sharedClassName =
|
|
113
|
+
"min-h-12 w-full rounded-cap-primary border border-[var(--color-border-default)] bg-[var(--color-surface-primary)] px-4 py-3 text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] outline-none transition focus:border-[var(--color-brand-primary)] focus:border-2";
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<label className="flex w-full flex-col gap-2">
|
|
117
|
+
<span className="text-sm font-medium text-[var(--color-text-secondary)]">{label}</span>
|
|
118
|
+
<div className="flex items-end gap-2">
|
|
119
|
+
{multiline ? (
|
|
120
|
+
<textarea
|
|
121
|
+
className={cn(sharedClassName, "min-h-28 resize-y", className)}
|
|
122
|
+
value={String(value ?? "")}
|
|
123
|
+
onChange={(event) => onValueChange?.(event.target.value)}
|
|
124
|
+
placeholder={placeholder}
|
|
125
|
+
maxLength={maxLength}
|
|
126
|
+
/>
|
|
127
|
+
) : (
|
|
128
|
+
<input
|
|
129
|
+
className={cn(sharedClassName, className)}
|
|
130
|
+
value={String(value ?? "")}
|
|
131
|
+
onChange={(event) => onValueChange?.(event.target.value)}
|
|
132
|
+
placeholder={placeholder}
|
|
133
|
+
maxLength={maxLength}
|
|
134
|
+
type={type}
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
|
+
{trailingAction}
|
|
138
|
+
</div>
|
|
139
|
+
</label>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function SelectField({
|
|
144
|
+
label,
|
|
145
|
+
value,
|
|
146
|
+
options,
|
|
147
|
+
onValueChange,
|
|
148
|
+
}: {
|
|
149
|
+
label: string;
|
|
150
|
+
value: string;
|
|
151
|
+
options: Array<{ value: string; label: string }>;
|
|
152
|
+
onValueChange: (value: string) => void;
|
|
153
|
+
}) {
|
|
154
|
+
return (
|
|
155
|
+
<label className="flex w-full flex-col gap-2">
|
|
156
|
+
<span className="text-sm font-medium text-[var(--color-text-secondary)]">{label}</span>
|
|
157
|
+
<div className="relative">
|
|
158
|
+
<select
|
|
159
|
+
value={value}
|
|
160
|
+
onChange={(event) => onValueChange(event.target.value)}
|
|
161
|
+
className="min-h-12 w-full appearance-none rounded-cap-primary border border-[var(--color-border-default)] bg-[var(--color-surface-primary)] px-4 py-3 text-sm text-[var(--color-text-primary)] outline-none transition focus:border-2 focus:border-[var(--color-brand-primary)]"
|
|
162
|
+
>
|
|
163
|
+
{options.map((option) => (
|
|
164
|
+
<option key={option.value} value={option.value}>
|
|
165
|
+
{option.label}
|
|
166
|
+
</option>
|
|
167
|
+
))}
|
|
168
|
+
</select>
|
|
169
|
+
<Icon name="more" className="pointer-events-none absolute right-3 top-3.5 h-5 w-5 rotate-90 text-[var(--color-text-tertiary)]" />
|
|
170
|
+
</div>
|
|
171
|
+
</label>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function ToggleField({
|
|
176
|
+
label,
|
|
177
|
+
checked,
|
|
178
|
+
onChange,
|
|
179
|
+
}: {
|
|
180
|
+
label: string;
|
|
181
|
+
checked: boolean;
|
|
182
|
+
onChange: (value: boolean) => void;
|
|
183
|
+
}) {
|
|
184
|
+
return (
|
|
185
|
+
<div className="flex items-center justify-between gap-4 rounded-card border border-[var(--color-border-default)] bg-[var(--color-surface-primary)] px-4 py-3">
|
|
186
|
+
<span className="text-sm font-medium text-[var(--color-text-primary)]">{label}</span>
|
|
187
|
+
<input
|
|
188
|
+
type="checkbox"
|
|
189
|
+
role="switch"
|
|
190
|
+
checked={checked}
|
|
191
|
+
onChange={(event) => onChange(event.target.checked)}
|
|
192
|
+
className="h-5 w-10 accent-[var(--color-brand-primary)]"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function Avatar({
|
|
199
|
+
src,
|
|
200
|
+
name,
|
|
201
|
+
size = "md",
|
|
202
|
+
}: {
|
|
203
|
+
src?: string;
|
|
204
|
+
name: string;
|
|
205
|
+
size?: "sm" | "md" | "lg";
|
|
206
|
+
}) {
|
|
207
|
+
const sizeClass =
|
|
208
|
+
size === "sm"
|
|
209
|
+
? "h-10 w-10 text-xs"
|
|
210
|
+
: size === "lg"
|
|
211
|
+
? "h-[4.5rem] w-[4.5rem] text-lg"
|
|
212
|
+
: "h-12 w-12 text-sm";
|
|
213
|
+
|
|
214
|
+
return src ? (
|
|
215
|
+
<img
|
|
216
|
+
alt={name}
|
|
217
|
+
src={src}
|
|
218
|
+
className={cn("rounded-cap-primary object-cover", sizeClass)}
|
|
219
|
+
/>
|
|
220
|
+
) : (
|
|
221
|
+
<div className={cn("flex items-center justify-center rounded-cap-primary bg-[var(--color-surface-tertiary)] font-semibold text-[var(--color-text-secondary)]", sizeClass)}>
|
|
222
|
+
{getInitials(name)}
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function EmptyState({
|
|
228
|
+
title,
|
|
229
|
+
description,
|
|
230
|
+
action,
|
|
231
|
+
}: {
|
|
232
|
+
title: string;
|
|
233
|
+
description: string;
|
|
234
|
+
action?: ReactNode;
|
|
235
|
+
}) {
|
|
236
|
+
return (
|
|
237
|
+
<Surface className="p-8 text-center">
|
|
238
|
+
<div className="mx-auto flex max-w-md flex-col items-center gap-3">
|
|
239
|
+
<div className="rounded-cap-primary bg-[var(--color-surface-tertiary)] px-4 py-2 text-xs font-semibold uppercase tracking-[0.28em] text-[var(--color-text-secondary)]">
|
|
240
|
+
Empty
|
|
241
|
+
</div>
|
|
242
|
+
<h3 className="text-lg font-semibold text-[var(--color-text-primary)]">{title}</h3>
|
|
243
|
+
<p className="text-sm text-[var(--color-text-secondary)]">{description}</p>
|
|
244
|
+
{action}
|
|
245
|
+
</div>
|
|
246
|
+
</Surface>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function ErrorState({
|
|
251
|
+
title,
|
|
252
|
+
description,
|
|
253
|
+
}: {
|
|
254
|
+
title: string;
|
|
255
|
+
description: string;
|
|
256
|
+
}) {
|
|
257
|
+
return (
|
|
258
|
+
<div className="rounded-surface border border-[color:rgba(212,59,59,0.24)] bg-[color:rgba(212,59,59,0.08)] p-5 text-sm text-[var(--color-text-primary)]">
|
|
259
|
+
<h3 className="font-semibold">{title}</h3>
|
|
260
|
+
<p className="mt-1 text-[var(--color-text-secondary)]">{description}</p>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function FeedbackBanner({
|
|
266
|
+
title,
|
|
267
|
+
description,
|
|
268
|
+
onDismiss,
|
|
269
|
+
}: {
|
|
270
|
+
title: string;
|
|
271
|
+
description: string;
|
|
272
|
+
onDismiss: () => void;
|
|
273
|
+
}) {
|
|
274
|
+
return (
|
|
275
|
+
<div className="flex items-start justify-between gap-4 border border-[var(--color-border-default)] bg-[var(--color-surface-tertiary)] px-4 py-4">
|
|
276
|
+
<div>
|
|
277
|
+
<p className="font-semibold text-[var(--color-text-primary)]">{title}</p>
|
|
278
|
+
<p className="mt-1 text-sm text-[var(--color-text-secondary)]">{description}</p>
|
|
279
|
+
</div>
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
onClick={onDismiss}
|
|
283
|
+
className="interactive-press rounded-full p-1.5 text-[var(--color-text-secondary)]"
|
|
284
|
+
aria-label="Dismiss banner"
|
|
285
|
+
>
|
|
286
|
+
<Icon name="close" className="h-4 w-4" />
|
|
287
|
+
</button>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function SkeletonList({ count = 3, tall }: { count?: number; tall?: boolean }) {
|
|
293
|
+
return (
|
|
294
|
+
<div className="space-y-3">
|
|
295
|
+
{Array.from({ length: count }).map((_, index) => (
|
|
296
|
+
<div
|
|
297
|
+
key={index}
|
|
298
|
+
className={cn(
|
|
299
|
+
"skeleton rounded-card border border-[var(--color-border-default)] bg-[var(--color-surface-secondary)]",
|
|
300
|
+
tall ? "h-44" : "h-28",
|
|
301
|
+
)}
|
|
302
|
+
/>
|
|
303
|
+
))}
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function PillLink({
|
|
309
|
+
to,
|
|
310
|
+
children,
|
|
311
|
+
icon,
|
|
312
|
+
}: {
|
|
313
|
+
to: string;
|
|
314
|
+
children: ReactNode;
|
|
315
|
+
icon?: Parameters<typeof Icon>[0]["name"];
|
|
316
|
+
}) {
|
|
317
|
+
return (
|
|
318
|
+
<Link
|
|
319
|
+
to={to}
|
|
320
|
+
className="interactive-press rounded-cap-alternate border border-[var(--color-border-strong)] px-4 py-2 text-sm font-semibold text-[var(--color-text-primary)]"
|
|
321
|
+
>
|
|
322
|
+
<span className="inline-flex items-center gap-2">
|
|
323
|
+
{icon ? <Icon name={icon} className="h-4 w-4" /> : null}
|
|
324
|
+
{children}
|
|
325
|
+
</span>
|
|
326
|
+
</Link>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useNavigate } from "react-router";
|
|
3
|
+
import { useI18n } from "../i18n";
|
|
4
|
+
import { ActionButton, SelectField, TextField } from "../components/ui";
|
|
5
|
+
import { Icon } from "../lib/icons";
|
|
6
|
+
import { useSizeClass } from "../lib/utils";
|
|
7
|
+
import { useAppStore } from "../state/store";
|
|
8
|
+
|
|
9
|
+
export function CreatePostFlow() {
|
|
10
|
+
const { t } = useI18n();
|
|
11
|
+
const navigate = useNavigate();
|
|
12
|
+
const sizeClass = useSizeClass();
|
|
13
|
+
const state = useAppStore();
|
|
14
|
+
const [body, setBody] = useState("");
|
|
15
|
+
const [media, setMedia] = useState("");
|
|
16
|
+
const [audience, setAudience] = useState("public");
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="fixed inset-0 z-40 bg-[rgba(28,27,26,0.32)] px-3 pb-3 pt-20 md:px-6">
|
|
20
|
+
<div
|
|
21
|
+
className={`mx-auto max-w-2xl border border-[var(--color-border-default)] bg-[var(--color-surface-primary)] shadow-lg ${
|
|
22
|
+
sizeClass === "compact" ? "fixed inset-x-0 bottom-0 rounded-t-[24px] p-5" : "rounded-surface p-6"
|
|
23
|
+
}`}
|
|
24
|
+
>
|
|
25
|
+
{sizeClass === "compact" ? <div className="mx-auto mb-4 h-1.5 w-16 rounded-full bg-[var(--color-border-strong)]" /> : null}
|
|
26
|
+
<div className="mb-6 flex items-center justify-between gap-4">
|
|
27
|
+
<div>
|
|
28
|
+
<p className="text-xs uppercase tracking-[0.28em] text-[var(--color-text-tertiary)]">{t("nav.create")}</p>
|
|
29
|
+
<h2 className="mt-2 text-2xl font-semibold text-[var(--color-text-primary)]">Compose</h2>
|
|
30
|
+
</div>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={() => navigate(-1)}
|
|
34
|
+
className="interactive-press rounded-cap-primary border border-[var(--color-border-default)] bg-[var(--color-surface-secondary)] p-2"
|
|
35
|
+
aria-label="Close"
|
|
36
|
+
>
|
|
37
|
+
<Icon name="close" className="h-5 w-5" />
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="space-y-4">
|
|
42
|
+
<TextField
|
|
43
|
+
label={t("create_post.body_placeholder")}
|
|
44
|
+
value={body}
|
|
45
|
+
multiline
|
|
46
|
+
onValueChange={setBody}
|
|
47
|
+
placeholder={t("create_post.body_placeholder")}
|
|
48
|
+
maxLength={4000}
|
|
49
|
+
/>
|
|
50
|
+
<SelectField
|
|
51
|
+
label={t("create_post.audience")}
|
|
52
|
+
value={audience}
|
|
53
|
+
options={[
|
|
54
|
+
{ value: "public", label: t("create_post.audience_public") },
|
|
55
|
+
{ value: "followers", label: t("create_post.audience_followers") },
|
|
56
|
+
]}
|
|
57
|
+
onValueChange={setAudience}
|
|
58
|
+
/>
|
|
59
|
+
<TextField
|
|
60
|
+
label={t("create_post.add_image")}
|
|
61
|
+
value={media}
|
|
62
|
+
onValueChange={setMedia}
|
|
63
|
+
placeholder="Paste an image URL"
|
|
64
|
+
/>
|
|
65
|
+
<ActionButton variant="secondary" icon="image" onClick={() => setMedia("https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=1200&q=80")}>
|
|
66
|
+
{t("create_post.add_image")}
|
|
67
|
+
</ActionButton>
|
|
68
|
+
<ActionButton
|
|
69
|
+
variant="primary"
|
|
70
|
+
fullWidth
|
|
71
|
+
onClick={() => {
|
|
72
|
+
if (!body.trim()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
state.createPost({ body: body.trim(), media: media.trim(), audience });
|
|
76
|
+
state.showToast(t("create_post.success"));
|
|
77
|
+
navigate("/home");
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{t("create_post.publish")}
|
|
81
|
+
</ActionButton>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type PropsWithChildren,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
} from "react";
|
|
8
|
+
import en from "./locales/en.json";
|
|
9
|
+
import ru from "./locales/ru.json";
|
|
10
|
+
import uz from "./locales/uz.json";
|
|
11
|
+
import { useAppStore } from "./state/store";
|
|
12
|
+
import type { LocaleCode } from "./lib/tokens";
|
|
13
|
+
|
|
14
|
+
type Messages = Record<string, string>;
|
|
15
|
+
|
|
16
|
+
const bundles: Record<LocaleCode, Messages> = {
|
|
17
|
+
en,
|
|
18
|
+
ru,
|
|
19
|
+
uz,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type I18nValue = {
|
|
23
|
+
locale: LocaleCode;
|
|
24
|
+
direction: "ltr" | "rtl";
|
|
25
|
+
t: (key: string) => string;
|
|
26
|
+
setLocale: (locale: LocaleCode) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const I18nContext = createContext<I18nValue | null>(null);
|
|
30
|
+
|
|
31
|
+
export function I18nProvider({ children }: PropsWithChildren) {
|
|
32
|
+
const locale = useAppStore((state) => state.locale);
|
|
33
|
+
const setLocale = useAppStore((state) => state.setLocale);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
document.documentElement.lang = locale;
|
|
37
|
+
document.documentElement.dir = (bundles[locale].$direction as "ltr" | "rtl") ?? "ltr";
|
|
38
|
+
}, [locale]);
|
|
39
|
+
|
|
40
|
+
const value = useMemo<I18nValue>(() => {
|
|
41
|
+
const messages = bundles[locale] ?? bundles.en;
|
|
42
|
+
return {
|
|
43
|
+
locale,
|
|
44
|
+
direction: (messages.$direction as "ltr" | "rtl") ?? "ltr",
|
|
45
|
+
t: (key: string) => messages[key] ?? bundles.en[key] ?? key,
|
|
46
|
+
setLocale,
|
|
47
|
+
};
|
|
48
|
+
}, [locale, setLocale]);
|
|
49
|
+
|
|
50
|
+
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useI18n() {
|
|
54
|
+
const context = useContext(I18nContext);
|
|
55
|
+
if (!context) {
|
|
56
|
+
throw new Error("useI18n must be used within I18nProvider");
|
|
57
|
+
}
|
|
58
|
+
return context;
|
|
59
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { SVGProps } from "react";
|
|
2
|
+
|
|
3
|
+
type IconProps = SVGProps<SVGSVGElement> & {
|
|
4
|
+
name:
|
|
5
|
+
| "home"
|
|
6
|
+
| "discover"
|
|
7
|
+
| "notifications"
|
|
8
|
+
| "profile"
|
|
9
|
+
| "search"
|
|
10
|
+
| "create_post"
|
|
11
|
+
| "like"
|
|
12
|
+
| "like_fill"
|
|
13
|
+
| "comment"
|
|
14
|
+
| "share"
|
|
15
|
+
| "bookmark"
|
|
16
|
+
| "bookmark_fill"
|
|
17
|
+
| "more"
|
|
18
|
+
| "send"
|
|
19
|
+
| "camera"
|
|
20
|
+
| "image"
|
|
21
|
+
| "edit"
|
|
22
|
+
| "check"
|
|
23
|
+
| "back"
|
|
24
|
+
| "close"
|
|
25
|
+
| "settings";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function Icon({ name, className, ...props }: IconProps) {
|
|
29
|
+
const shared = {
|
|
30
|
+
viewBox: "0 0 24 24",
|
|
31
|
+
fill: "none",
|
|
32
|
+
stroke: "currentColor",
|
|
33
|
+
strokeWidth: 1.8,
|
|
34
|
+
strokeLinecap: "round" as const,
|
|
35
|
+
strokeLinejoin: "round" as const,
|
|
36
|
+
className,
|
|
37
|
+
"aria-hidden": true,
|
|
38
|
+
...props,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
switch (name) {
|
|
42
|
+
case "home":
|
|
43
|
+
return <svg {...shared}><path d="M3 10.5 12 3l9 7.5" /><path d="M5 10v10h14V10" /></svg>;
|
|
44
|
+
case "discover":
|
|
45
|
+
return <svg {...shared}><circle cx="12" cy="12" r="8" /><path d="m15.5 8.5-2.4 5.4-5.6 2.1 2.5-5.5Z" /></svg>;
|
|
46
|
+
case "notifications":
|
|
47
|
+
return <svg {...shared}><path d="M6 17h12l-1.4-1.7A4.5 4.5 0 0 1 15.5 12V10a3.5 3.5 0 1 0-7 0v2c0 1.2-.4 2.3-1.1 3.3Z" /><path d="M10 18.5a2 2 0 0 0 4 0" /></svg>;
|
|
48
|
+
case "profile":
|
|
49
|
+
return <svg {...shared}><circle cx="12" cy="8" r="3.5" /><path d="M5 20a7 7 0 0 1 14 0" /></svg>;
|
|
50
|
+
case "search":
|
|
51
|
+
return <svg {...shared}><circle cx="11" cy="11" r="6" /><path d="m20 20-3.5-3.5" /></svg>;
|
|
52
|
+
case "create_post":
|
|
53
|
+
return <svg {...shared}><circle cx="12" cy="12" r="8.5" /><path d="M12 8v8" /><path d="M8 12h8" /></svg>;
|
|
54
|
+
case "like":
|
|
55
|
+
return <svg {...shared}><path d="M12 20s-7-4.3-7-9.5A4 4 0 0 1 12 8a4 4 0 0 1 7 2.5C19 15.7 12 20 12 20Z" /></svg>;
|
|
56
|
+
case "like_fill":
|
|
57
|
+
return <svg viewBox="0 0 24 24" className={className} aria-hidden="true" {...props}><path fill="currentColor" d="M12 20s-7-4.3-7-9.5A4 4 0 0 1 12 8a4 4 0 0 1 7 2.5C19 15.7 12 20 12 20Z" /></svg>;
|
|
58
|
+
case "comment":
|
|
59
|
+
return <svg {...shared}><path d="M5 6h14v9H9l-4 3V6Z" /></svg>;
|
|
60
|
+
case "share":
|
|
61
|
+
return <svg {...shared}><path d="M14 6h5v5" /><path d="M10 14 19 5" /><path d="M19 13v5H5V5h5" /></svg>;
|
|
62
|
+
case "bookmark":
|
|
63
|
+
return <svg {...shared}><path d="M7 4h10v16l-5-3-5 3V4Z" /></svg>;
|
|
64
|
+
case "bookmark_fill":
|
|
65
|
+
return <svg viewBox="0 0 24 24" className={className} aria-hidden="true" {...props}><path fill="currentColor" d="M7 4h10v16l-5-3-5 3V4Z" /></svg>;
|
|
66
|
+
case "more":
|
|
67
|
+
return <svg {...shared}><circle cx="12" cy="6" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="12" cy="18" r="1.5" /></svg>;
|
|
68
|
+
case "send":
|
|
69
|
+
return <svg {...shared}><path d="M4 20 20 12 4 4l2.5 8Z" /></svg>;
|
|
70
|
+
case "camera":
|
|
71
|
+
return <svg {...shared}><path d="M5 8h3l2-2h4l2 2h3v10H5Z" /><circle cx="12" cy="13" r="3.5" /></svg>;
|
|
72
|
+
case "image":
|
|
73
|
+
return <svg {...shared}><rect x="4" y="5" width="16" height="14" rx="2" /><circle cx="9" cy="10" r="1.2" /><path d="m7 17 4-4 3 3 3-4 2 5" /></svg>;
|
|
74
|
+
case "edit":
|
|
75
|
+
return <svg {...shared}><path d="m5 19 3.5-.7L18 8.8 15.2 6 5.7 15.5Z" /><path d="m13.8 7.2 3 3" /></svg>;
|
|
76
|
+
case "check":
|
|
77
|
+
return <svg {...shared}><path d="m5 13 4 4L19 7" /></svg>;
|
|
78
|
+
case "back":
|
|
79
|
+
return <svg {...shared}><path d="m15 18-6-6 6-6" /></svg>;
|
|
80
|
+
case "close":
|
|
81
|
+
return <svg {...shared}><path d="m6 6 12 12" /><path d="M18 6 6 18" /></svg>;
|
|
82
|
+
case "settings":
|
|
83
|
+
return <svg {...shared}><circle cx="12" cy="12" r="2.5" /><path d="M19 12a7.2 7.2 0 0 0-.1-1l2-1.5-2-3.5-2.4 1a7.3 7.3 0 0 0-1.8-1L14.5 3h-5L9 6a7.3 7.3 0 0 0-1.8 1l-2.4-1-2 3.5 2 1.5a7.2 7.2 0 0 0 0 2l-2 1.5 2 3.5 2.4-1c.6.4 1.2.7 1.8 1l.5 3h5l.5-3c.6-.3 1.2-.6 1.8-1l2.4 1 2-3.5-2-1.5c.1-.3.1-.7.1-1Z" /></svg>;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const tokens = {
|
|
2
|
+
colors: {
|
|
3
|
+
brandPrimary: "#1C1B1A",
|
|
4
|
+
brandPrimaryOn: "#FFFFFF",
|
|
5
|
+
brandAccent: "#5B52A3",
|
|
6
|
+
brandAccentOn: "#FFFFFF",
|
|
7
|
+
surfacePrimary: "#FAF8F5",
|
|
8
|
+
surfaceSecondary: "#F3F0EB",
|
|
9
|
+
surfaceTertiary: "#EBE7E0",
|
|
10
|
+
textPrimary: "#1C1B1A",
|
|
11
|
+
textSecondary: "#6B6966",
|
|
12
|
+
textTertiary: "#9E9A95",
|
|
13
|
+
success: "#2D9D5E",
|
|
14
|
+
warning: "#D4920E",
|
|
15
|
+
danger: "#D43B3B",
|
|
16
|
+
info: "#3B82D4",
|
|
17
|
+
borderDefault: "#E0DCD6",
|
|
18
|
+
borderStrong: "#C5C0B8",
|
|
19
|
+
},
|
|
20
|
+
shadows: {
|
|
21
|
+
sm: "0 1px 3px rgba(28, 27, 26, 0.08)",
|
|
22
|
+
md: "0 4px 12px rgba(28, 27, 26, 0.12)",
|
|
23
|
+
lg: "0 8px 24px rgba(28, 27, 26, 0.16)",
|
|
24
|
+
},
|
|
25
|
+
radius: {
|
|
26
|
+
capPrimary: "2px 24px 2px 24px",
|
|
27
|
+
capAlternate: "24px 2px 24px 2px",
|
|
28
|
+
card: "3px 20px 3px 20px",
|
|
29
|
+
surface: "3px 24px 3px 24px",
|
|
30
|
+
sheet: "24px 24px 0 0",
|
|
31
|
+
},
|
|
32
|
+
spacing: {
|
|
33
|
+
xxs: 2,
|
|
34
|
+
xs: 4,
|
|
35
|
+
sm: 8,
|
|
36
|
+
md: 16,
|
|
37
|
+
lg: 24,
|
|
38
|
+
xl: 32,
|
|
39
|
+
xxl: 48,
|
|
40
|
+
},
|
|
41
|
+
typography: {
|
|
42
|
+
display: { size: 32, weight: 700, lineHeight: 1.2 },
|
|
43
|
+
headingLg: { size: 24, weight: 600, lineHeight: 1.3 },
|
|
44
|
+
headingMd: { size: 20, weight: 600, lineHeight: 1.3 },
|
|
45
|
+
headingSm: { size: 16, weight: 600, lineHeight: 1.4 },
|
|
46
|
+
body: { size: 16, weight: 400, lineHeight: 1.5 },
|
|
47
|
+
bodySm: { size: 14, weight: 400, lineHeight: 1.5 },
|
|
48
|
+
caption: { size: 12, weight: 400, lineHeight: 1.4 },
|
|
49
|
+
button: { size: 16, weight: 600, lineHeight: 1.0 },
|
|
50
|
+
},
|
|
51
|
+
breakpoints: {
|
|
52
|
+
compactMax: 600,
|
|
53
|
+
regularMax: 1024,
|
|
54
|
+
},
|
|
55
|
+
motion: {
|
|
56
|
+
quick: 200,
|
|
57
|
+
normal: 300,
|
|
58
|
+
slow: 500,
|
|
59
|
+
easing: {
|
|
60
|
+
default: "ease-out",
|
|
61
|
+
enter: "ease-out",
|
|
62
|
+
exit: "ease-in",
|
|
63
|
+
emphasis: "cubic-bezier(0.2, 0, 0, 1)",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
export type LocaleCode = "en" | "ru" | "uz";
|
|
69
|
+
export type ThemePreference = "system" | "light" | "dark";
|
|
70
|
+
export type SizeClass = "compact" | "regular" | "expanded";
|