git commit -m "feat: initial release of Strata framework v0.1.0
- Static compiler with STRC pattern (Static Template Resolution with
Compartmentalized Layers)
- Template syntax: { } interpolation, { s-for }, { s-if/s-elif/s-else
}
- File types: .strata, .compiler.sts, .service.sts, .api.sts, .sts,
.scss
- CLI tools: strata dev, strata build, strata g (generators)
- create-strata scaffolding CLI with Pokemon API example
- Dev server with WebSocket HMR (Hot Module Replacement)
- Documentation: README, ARCHITECTURE, CHANGELOG, CONTRIBUTING,
LICENSE
This commit is contained in:
16
examples/basic-app/package.json
Normal file
16
examples/basic-app/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "strata-example-basic",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "strata dev",
|
||||
"build": "strata build",
|
||||
"preview": "strata preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"strata": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
144
examples/basic-app/src/assets/styles/_mixins.scss
Normal file
144
examples/basic-app/src/assets/styles/_mixins.scss
Normal file
@@ -0,0 +1,144 @@
|
||||
// Responsive breakpoints
|
||||
@mixin sm {
|
||||
@media (min-width: $breakpoint-sm) { @content; }
|
||||
}
|
||||
|
||||
@mixin md {
|
||||
@media (min-width: $breakpoint-md) { @content; }
|
||||
}
|
||||
|
||||
@mixin lg {
|
||||
@media (min-width: $breakpoint-lg) { @content; }
|
||||
}
|
||||
|
||||
@mixin xl {
|
||||
@media (min-width: $breakpoint-xl) { @content; }
|
||||
}
|
||||
|
||||
// Flexbox helpers
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@mixin flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Grid helper
|
||||
@mixin grid($columns: 1, $gap: $spacing-md) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat($columns, 1fr);
|
||||
gap: $gap;
|
||||
}
|
||||
|
||||
@mixin auto-grid($min-width: 280px, $gap: $spacing-md) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax($min-width, 1fr));
|
||||
gap: $gap;
|
||||
}
|
||||
|
||||
// Typography
|
||||
@mixin text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin line-clamp($lines: 2) {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $lines;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Interactive states
|
||||
@mixin hover-lift($distance: -2px) {
|
||||
transition: transform $transition-normal, box-shadow $transition-normal;
|
||||
|
||||
&:hover {
|
||||
transform: translateY($distance);
|
||||
box-shadow: $shadow-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin focus-ring($color: var(--primary)) {
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba($color, 0.3);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba($color, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// Card styles
|
||||
@mixin card($padding: $spacing-lg) {
|
||||
background: var(--card-bg);
|
||||
border-radius: $radius-lg;
|
||||
padding: $padding;
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
|
||||
// Button base
|
||||
@mixin button-base {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-sm;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: none;
|
||||
border-radius: $radius-md;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background $transition-fast, transform $transition-fast;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// Skeleton loading
|
||||
@mixin skeleton {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--border) 25%,
|
||||
var(--secondary) 50%,
|
||||
var(--border) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
// Visually hidden (accessible)
|
||||
@mixin visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
78
examples/basic-app/src/assets/styles/_variables.scss
Normal file
78
examples/basic-app/src/assets/styles/_variables.scss
Normal file
@@ -0,0 +1,78 @@
|
||||
// Color palette
|
||||
$primary: #6366f1;
|
||||
$primary-dark: #4f46e5;
|
||||
$secondary: #e5e7eb;
|
||||
$secondary-dark: #d1d5db;
|
||||
|
||||
// Text colors
|
||||
$text-primary: #1f2937;
|
||||
$text-secondary: #4b5563;
|
||||
$text-muted: #9ca3af;
|
||||
|
||||
// Background colors
|
||||
$card-bg: #ffffff;
|
||||
$code-bg: #f3f4f6;
|
||||
$border: #e5e7eb;
|
||||
|
||||
// CSS custom properties (for runtime theming)
|
||||
:root {
|
||||
--primary: #{$primary};
|
||||
--primary-dark: #{$primary-dark};
|
||||
--secondary: #{$secondary};
|
||||
--secondary-dark: #{$secondary-dark};
|
||||
|
||||
--text-primary: #{$text-primary};
|
||||
--text-secondary: #{$text-secondary};
|
||||
--text-muted: #{$text-muted};
|
||||
|
||||
--card-bg: #{$card-bg};
|
||||
--code-bg: #{$code-bg};
|
||||
--border: #{$border};
|
||||
}
|
||||
|
||||
// Dark mode
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--primary: #818cf8;
|
||||
--primary-dark: #6366f1;
|
||||
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #6b7280;
|
||||
|
||||
--card-bg: #1f2937;
|
||||
--code-bg: #374151;
|
||||
--border: #374151;
|
||||
}
|
||||
}
|
||||
|
||||
// Spacing
|
||||
$spacing-xs: 0.25rem;
|
||||
$spacing-sm: 0.5rem;
|
||||
$spacing-md: 1rem;
|
||||
$spacing-lg: 1.5rem;
|
||||
$spacing-xl: 2rem;
|
||||
$spacing-2xl: 3rem;
|
||||
|
||||
// Border radius
|
||||
$radius-sm: 4px;
|
||||
$radius-md: 8px;
|
||||
$radius-lg: 12px;
|
||||
$radius-xl: 16px;
|
||||
$radius-full: 9999px;
|
||||
|
||||
// Shadows
|
||||
$shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
$shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
$shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
|
||||
// Transitions
|
||||
$transition-fast: 150ms ease;
|
||||
$transition-normal: 200ms ease;
|
||||
$transition-slow: 300ms ease;
|
||||
|
||||
// Breakpoints
|
||||
$breakpoint-sm: 640px;
|
||||
$breakpoint-md: 768px;
|
||||
$breakpoint-lg: 1024px;
|
||||
$breakpoint-xl: 1280px;
|
||||
124
examples/basic-app/src/assets/styles/global.scss
Normal file
124
examples/basic-app/src/assets/styles/global.scss
Normal file
@@ -0,0 +1,124 @@
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
// Reset
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
background: var(--secondary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// Typography
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1.3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 { font-size: 2.5rem; }
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.5rem; }
|
||||
h4 { font-size: 1.25rem; }
|
||||
h5 { font-size: 1rem; }
|
||||
h6 { font-size: 0.875rem; }
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
// Code
|
||||
code {
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 0.9em;
|
||||
background: var(--code-bg);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--code-bg);
|
||||
padding: $spacing-md;
|
||||
border-radius: $radius-md;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Images
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
// Lists
|
||||
ul, ol {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
// Focus styles
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
// Selection
|
||||
::selection {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
// App container
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// Utility classes
|
||||
.sr-only {
|
||||
@include visually-hidden;
|
||||
}
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.mt-1 { margin-top: $spacing-sm; }
|
||||
.mt-2 { margin-top: $spacing-md; }
|
||||
.mt-3 { margin-top: $spacing-lg; }
|
||||
.mt-4 { margin-top: $spacing-xl; }
|
||||
|
||||
.mb-1 { margin-bottom: $spacing-sm; }
|
||||
.mb-2 { margin-bottom: $spacing-md; }
|
||||
.mb-3 { margin-bottom: $spacing-lg; }
|
||||
.mb-4 { margin-bottom: $spacing-xl; }
|
||||
158
examples/basic-app/src/components/UserCard.strata
Normal file
158
examples/basic-app/src/components/UserCard.strata
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="user-card" s-if="user">
|
||||
<div class="user-avatar">
|
||||
<img :src="avatarUrl" :alt="user.name" />
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<h3>{{ user.name }}</h3>
|
||||
<p class="email">{{ user.email }}</p>
|
||||
<p class="company" s-if="user.company">
|
||||
{{ user.company.name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="user-actions">
|
||||
<button @click="viewProfile" class="btn-primary">
|
||||
View Profile
|
||||
</button>
|
||||
<button @click="toggleFavorite" class="btn-secondary">
|
||||
{{ isFavorite ? 'Unfavorite' : 'Favorite' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-card skeleton" s-else>
|
||||
Loading...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useStore } from 'strata';
|
||||
import { userStore } from '@stores/user';
|
||||
import { favoritesStore } from '@stores/favorites';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
userId: { type: String, required: true }
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const user = useStore(userStore, state => state.users[props.userId]);
|
||||
const isFavorite = useStore(favoritesStore, state =>
|
||||
state.favorites.includes(props.userId)
|
||||
);
|
||||
|
||||
const avatarUrl = `https://api.dicebear.com/7.x/avataaars/svg?seed=${props.userId}`;
|
||||
|
||||
const viewProfile = () => {
|
||||
strata.navigate(`/users/${props.userId}`);
|
||||
};
|
||||
|
||||
const toggleFavorite = () => {
|
||||
if (isFavorite) {
|
||||
favoritesStore.remove(props.userId);
|
||||
} else {
|
||||
favoritesStore.add(props.userId);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
user,
|
||||
isFavorite,
|
||||
avatarUrl,
|
||||
viewProfile,
|
||||
toggleFavorite,
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.user-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
background: var(--card-bg, #fff);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&.skeleton {
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin: 0 auto 1rem;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.email {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.company {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
|
||||
&.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: var(--secondary);
|
||||
color: var(--text-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--secondary-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
9
examples/basic-app/src/injectedscripts/analytics.js
Normal file
9
examples/basic-app/src/injectedscripts/analytics.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/* @position: head */
|
||||
/* @priority: 10 */
|
||||
|
||||
<!-- Custom Analytics -->
|
||||
<script>
|
||||
window.analyticsQueue = window.analyticsQueue || [];
|
||||
function analytics() { analyticsQueue.push(arguments); }
|
||||
analytics('init', { app: 'strata-example' });
|
||||
</script>
|
||||
9
examples/basic-app/src/injectedscripts/gtm-noscript.js
Normal file
9
examples/basic-app/src/injectedscripts/gtm-noscript.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/* @position: body */
|
||||
/* @priority: 1 */
|
||||
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript>
|
||||
<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe>
|
||||
</noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
12
examples/basic-app/src/injectedscripts/gtm.js
Normal file
12
examples/basic-app/src/injectedscripts/gtm.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/* @position: head */
|
||||
/* @priority: 1 */
|
||||
|
||||
<!-- Google Tag Manager -->
|
||||
<script>
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','GTM-XXXXXXX');
|
||||
</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
148
examples/basic-app/src/pages/index.strata
Normal file
148
examples/basic-app/src/pages/index.strata
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<main class="home-page">
|
||||
<header class="hero">
|
||||
<h1>Welcome to Strata</h1>
|
||||
<p>A fast, modern framework for building web applications</p>
|
||||
</header>
|
||||
|
||||
<section class="users-section">
|
||||
<h2>Users</h2>
|
||||
|
||||
<!-- Smart fetch with loading state -->
|
||||
<div
|
||||
s-fetch="/users"
|
||||
s-as="users"
|
||||
s-loading="loading"
|
||||
s-error="error"
|
||||
>
|
||||
<div class="loading-state" s-if="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading users...</p>
|
||||
</div>
|
||||
|
||||
<div class="error-state" s-else-if="error">
|
||||
<p>Failed to load users: {{ error.message }}</p>
|
||||
<button @click="refetch">Try Again</button>
|
||||
</div>
|
||||
|
||||
<div class="users-grid" s-else>
|
||||
<UserCard
|
||||
s-for="user in users"
|
||||
:key="user.id"
|
||||
:userId="user.id"
|
||||
s-client:visible
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-section">
|
||||
<h2>Tab Info</h2>
|
||||
<p>Current Tab ID: <code>{{ tabId }}</code></p>
|
||||
<p>Connected Tabs: <code>{{ tabCount }}</code></p>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { strata, useStore } from 'strata';
|
||||
import { userStore } from '@stores/user';
|
||||
import UserCard from '@components/UserCard.strata';
|
||||
|
||||
export default {
|
||||
components: { UserCard },
|
||||
|
||||
setup() {
|
||||
const tabId = strata.tabId;
|
||||
const tabCount = ref(1);
|
||||
|
||||
// Listen for tab changes
|
||||
strata.onBroadcast('tab:joined', (data) => {
|
||||
tabCount.value = data.tabCount;
|
||||
});
|
||||
|
||||
strata.onBroadcast('tab:left', (data) => {
|
||||
tabCount.value = data.tabCount;
|
||||
});
|
||||
|
||||
return {
|
||||
tabId,
|
||||
tabCount,
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.home-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.users-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.users-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
background: var(--card-bg);
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
|
||||
code {
|
||||
background: var(--code-bg);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
55
examples/basic-app/src/stores/favorites.sts
Normal file
55
examples/basic-app/src/stores/favorites.sts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Favorites Store
|
||||
* Manages user favorites with tab-specific and shared state
|
||||
*/
|
||||
|
||||
import { createStore } from 'strata';
|
||||
|
||||
interface FavoritesState {
|
||||
favorites: string[]; // User IDs
|
||||
recentlyViewed: string[]; // Tab-specific
|
||||
}
|
||||
|
||||
export const favoritesStore = createStore<FavoritesState>('favorites', {
|
||||
state: {
|
||||
favorites: [],
|
||||
recentlyViewed: [],
|
||||
},
|
||||
|
||||
actions: {
|
||||
add(userId: string) {
|
||||
if (!this.favorites.includes(userId)) {
|
||||
this.favorites = [...this.favorites, userId];
|
||||
}
|
||||
},
|
||||
|
||||
remove(userId: string) {
|
||||
this.favorites = this.favorites.filter(id => id !== userId);
|
||||
},
|
||||
|
||||
toggle(userId: string) {
|
||||
if (this.favorites.includes(userId)) {
|
||||
this.remove(userId);
|
||||
} else {
|
||||
this.add(userId);
|
||||
}
|
||||
},
|
||||
|
||||
addToRecent(userId: string) {
|
||||
// Keep only last 10
|
||||
const recent = this.recentlyViewed.filter(id => id !== userId);
|
||||
this.recentlyViewed = [userId, ...recent].slice(0, 10);
|
||||
},
|
||||
|
||||
clearRecent() {
|
||||
this.recentlyViewed = [];
|
||||
},
|
||||
},
|
||||
|
||||
// Favorites are encrypted and shared across tabs
|
||||
encrypt: ['favorites'],
|
||||
persist: true,
|
||||
shared: ['favorites'], // Sync favorites across all tabs
|
||||
|
||||
// recentlyViewed stays tab-specific (not in shared)
|
||||
});
|
||||
100
examples/basic-app/src/stores/user.sts
Normal file
100
examples/basic-app/src/stores/user.sts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* User Store
|
||||
* Manages user data with encryption and cross-tab sync
|
||||
*/
|
||||
|
||||
import { createStore, strata } from 'strata';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
website?: string;
|
||||
company?: {
|
||||
name: string;
|
||||
catchPhrase?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UserState {
|
||||
users: Record<string, User>;
|
||||
currentUserId: string | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const userStore = createStore<UserState>('user', {
|
||||
state: {
|
||||
users: {},
|
||||
currentUserId: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchUsers() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const users = await strata.fetch.get<User[]>('/users');
|
||||
|
||||
// Index users by ID for fast lookup
|
||||
const usersById: Record<string, User> = {};
|
||||
for (const user of users) {
|
||||
usersById[user.id] = user;
|
||||
}
|
||||
|
||||
this.users = usersById;
|
||||
} catch (err) {
|
||||
this.error = err instanceof Error ? err.message : 'Failed to fetch users';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchUser(id: string) {
|
||||
if (this.users[id]) {
|
||||
return this.users[id]; // Already cached
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await strata.fetch.get<User>(`/users/${id}`);
|
||||
this.users[id] = user;
|
||||
return user;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentUser(id: string | null) {
|
||||
this.currentUserId = id;
|
||||
},
|
||||
|
||||
clearUsers() {
|
||||
this.users = {};
|
||||
this.currentUserId = null;
|
||||
},
|
||||
},
|
||||
|
||||
// Encrypt sensitive data
|
||||
encrypt: ['currentUserId'],
|
||||
|
||||
// Persist user cache across page reloads
|
||||
persist: ['users'],
|
||||
|
||||
// Share current user across all tabs
|
||||
shared: ['currentUserId'],
|
||||
});
|
||||
|
||||
// Computed values (derived state)
|
||||
export const getCurrentUser = () => {
|
||||
const state = userStore.getState();
|
||||
if (!state.currentUserId) return null;
|
||||
return state.users[state.currentUserId] || null;
|
||||
};
|
||||
|
||||
export const getUserCount = () => {
|
||||
return Object.keys(userStore.getState().users).length;
|
||||
};
|
||||
21
examples/basic-app/strataconfig.ts
Normal file
21
examples/basic-app/strataconfig.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'strata';
|
||||
|
||||
export default defineConfig({
|
||||
app: {
|
||||
title: 'Strata Example App',
|
||||
description: 'A simple example using Strata Framework',
|
||||
},
|
||||
|
||||
api: {
|
||||
baseUrl: 'https://jsonplaceholder.typicode.com',
|
||||
cache: {
|
||||
enabled: true,
|
||||
defaultTTL: '5m',
|
||||
},
|
||||
},
|
||||
|
||||
state: {
|
||||
encrypt: true,
|
||||
persist: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user