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:
2026-01-16 09:01:29 -05:00
commit 9e451469f5
48 changed files with 15605 additions and 0 deletions

View 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"
}
}

View 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;
}

View 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;

View 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; }

View 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>

View 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>

View 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) -->

View 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 -->

View 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>

View 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)
});

View 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;
};

View 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,
},
});