get a nice developement setup going and add main page and recipe page start
This commit is contained in:
parent
172934e8a0
commit
0f625dae1f
52
.air.toml
Normal file
52
.air.toml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#:schema https://json.schemastore.org/any.json
|
||||||
|
|
||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./first"
|
||||||
|
cmd = "$HOME/bin/jai/bin/jai-linux first.jai"
|
||||||
|
delay = 1000
|
||||||
|
exclude_file = []
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["jai"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
silent = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
1
components/index.jai
Normal file
1
components/index.jai
Normal file
@ -0,0 +1 @@
|
|||||||
|
#load "nav.jai";
|
||||||
16
components/nav.jai
Normal file
16
components/nav.jai
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
add_navbar :: (builder: *String_Builder) {
|
||||||
|
print_to_builder(builder, navbar, "OMAKASE");
|
||||||
|
}
|
||||||
|
|
||||||
|
#scope_file
|
||||||
|
|
||||||
|
navbar : string = #string DONE
|
||||||
|
<nav>
|
||||||
|
<div class="brand">%</div>
|
||||||
|
<div class="nav-actions">
|
||||||
|
<a href="index.html" class="active">Collection</a>
|
||||||
|
<a href="cookbook.html">Cookbooks</a>
|
||||||
|
<a href="atelier.html">Atelier</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
DONE
|
||||||
37
data.jai
Normal file
37
data.jai
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
State :: struct {
|
||||||
|
idTicker : int = 0;
|
||||||
|
recipes : [..]Recipe;
|
||||||
|
};
|
||||||
|
|
||||||
|
get_id :: () -> string {
|
||||||
|
state.idTicker += 1;
|
||||||
|
return sprint("REF-%", state.idTicker);
|
||||||
|
}
|
||||||
|
|
||||||
|
Component :: struct {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Recipe :: struct {
|
||||||
|
id : string;
|
||||||
|
name : string = "New recipe!";
|
||||||
|
category : string = "Breakfast";
|
||||||
|
image : string = "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?q=80&w=500&auto=format&fit=crop";
|
||||||
|
duration : int = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
state : State;
|
||||||
|
|
||||||
|
init_data :: () {
|
||||||
|
recipe := Recipe.{};
|
||||||
|
recipe.id = get_id();
|
||||||
|
array_add(*state.recipes, recipe);
|
||||||
|
recipe.id = get_id();
|
||||||
|
array_add(*state.recipes, recipe);
|
||||||
|
recipe.id = get_id();
|
||||||
|
array_add(*state.recipes, recipe);
|
||||||
|
recipe.id = get_id();
|
||||||
|
array_add(*state.recipes, recipe);
|
||||||
|
recipe.id = get_id();
|
||||||
|
array_add(*state.recipes, recipe);
|
||||||
|
}
|
||||||
50
main.jai
50
main.jai
@ -1,12 +1,13 @@
|
|||||||
#import "Basic";
|
#import "Basic";
|
||||||
#import "Hash_Table";
|
#import "Hash_Table";
|
||||||
#import "Socket";
|
#import "Socket";
|
||||||
|
#import "Pool";
|
||||||
String :: #import "String";
|
String :: #import "String";
|
||||||
|
|
||||||
#load "server.jai";
|
#load "server.jai";
|
||||||
|
|
||||||
consoom :: (input: *string, delimiter: string) -> string {
|
consoom :: (input: *string, delimiter: string) -> string {
|
||||||
if input.count < 1 then return "INVALID READ";
|
if input.count < 1 then return "";
|
||||||
found, left, right := String.split_from_left(input.*, delimiter);
|
found, left, right := String.split_from_left(input.*, delimiter);
|
||||||
input.* = right;
|
input.* = right;
|
||||||
return left;
|
return left;
|
||||||
@ -17,6 +18,7 @@ Request :: struct {
|
|||||||
method : string;
|
method : string;
|
||||||
protocol : string;
|
protocol : string;
|
||||||
headers : Table(string, string);
|
headers : Table(string, string);
|
||||||
|
query : Table(string, string);
|
||||||
body : string;
|
body : string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,15 +31,27 @@ Response :: struct {
|
|||||||
|
|
||||||
parse_request :: (data: *string) -> Request {
|
parse_request :: (data: *string) -> Request {
|
||||||
req := New(Request);
|
req := New(Request);
|
||||||
req.method = consoom(data, " ");
|
req.method = consoom(data, " ");
|
||||||
req.route = consoom(data, " ");
|
full_route := consoom(data, " ");
|
||||||
|
|
||||||
|
ok, route, query := String.split_from_left(full_route, "?");
|
||||||
|
req.route = route;
|
||||||
|
|
||||||
|
curQuery := consoom(*query, "&");
|
||||||
|
while curQuery.count > 0 {
|
||||||
|
key := consoom(*curQuery, "=");
|
||||||
|
value := curQuery;
|
||||||
|
table_set(*req.query, key, value);
|
||||||
|
curQuery = consoom(*query, "&");
|
||||||
|
}
|
||||||
|
|
||||||
req.protocol = consoom(data, "\n");
|
req.protocol = consoom(data, "\n");
|
||||||
headers_string := consoom(data, "\r\n\r\n");
|
headers_string := consoom(data, "\r\n\r\n");
|
||||||
|
|
||||||
while headers_string.count > 0 {
|
while headers_string.count > 0 {
|
||||||
key := consoom(*headers_string, ": ");
|
key := consoom(*headers_string, ": ");
|
||||||
value := consoom(*headers_string, "\n");
|
value := consoom(*headers_string, "\n");
|
||||||
table_add(*req.headers, key, value);
|
table_set(*req.headers, key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.body = data.*;
|
req.body = data.*;
|
||||||
@ -52,7 +66,6 @@ serialize_response :: (res: *Response) -> string {
|
|||||||
builder : String_Builder;
|
builder : String_Builder;
|
||||||
print_to_builder(*builder, "% % OK", res.protocol, res.status);
|
print_to_builder(*builder, "% % OK", res.protocol, res.status);
|
||||||
for v, k : res.headers {
|
for v, k : res.headers {
|
||||||
print("%: %\n", k,v);
|
|
||||||
print_to_builder(*builder, "\n%: %", k, v);
|
print_to_builder(*builder, "\n%: %", k, v);
|
||||||
}
|
}
|
||||||
print_to_builder(*builder, "\r\n\r\n");
|
print_to_builder(*builder, "\r\n\r\n");
|
||||||
@ -85,14 +98,17 @@ siginfo_t :: struct { // This is not the correct size, but we don't instantiate
|
|||||||
si_addr: *void;
|
si_addr: *void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_killed : bool = false;
|
||||||
|
|
||||||
handle_signal :: (sig: s32, info: *siginfo_t, secret: *void) #c_call {
|
handle_signal :: (sig: s32, info: *siginfo_t, secret: *void) #c_call {
|
||||||
c : #Context;
|
c : #Context;
|
||||||
push_context(c) {
|
push_context c {
|
||||||
print("Graceful exit!!!\n");
|
print("Exiting gracefully.....\n");
|
||||||
close_and_reset(*main_socket);
|
|
||||||
}
|
}
|
||||||
|
is_killed = true;
|
||||||
}
|
}
|
||||||
main :: () {
|
main :: () {
|
||||||
|
init_server();
|
||||||
socket_init();
|
socket_init();
|
||||||
main_socket = socket(AF_INET, .SOCK_STREAM, .TCP);
|
main_socket = socket(AF_INET, .SOCK_STREAM, .TCP);
|
||||||
reuse : s32 = 1;
|
reuse : s32 = 1;
|
||||||
@ -112,9 +128,20 @@ main :: () {
|
|||||||
sa.sa_sigaction = handle_signal;
|
sa.sa_sigaction = handle_signal;
|
||||||
sigaction(2, *sa, null);
|
sigaction(2, *sa, null);
|
||||||
|
|
||||||
|
// Creating a pool and setting it as the allocator in the context.
|
||||||
|
// This means that we can do whatever the fuck after this, and we
|
||||||
|
// can release all the memory by releasing memory in the pool.
|
||||||
|
//
|
||||||
|
// We can also just reset the pool, meaning that we just reset pointer
|
||||||
|
// to the pool's start, but keep the pool's size. So allocating inside
|
||||||
|
// a request is basically free and we never actually free any memory or
|
||||||
|
// allocate any new memory (unless we go over the previous amount of max used memory).
|
||||||
|
reqpool : Pool;
|
||||||
|
set_allocators(*reqpool);
|
||||||
|
context.allocator = .{pool_allocator_proc, *reqpool};
|
||||||
|
|
||||||
while true {
|
while !is_killed {
|
||||||
rbuf : [2048 * 1000]u8;
|
rbuf : [2048 * 1000]u8; // @ToDo: This sucks, we should have some better thing here.
|
||||||
addr : sockaddr;
|
addr : sockaddr;
|
||||||
addr_len : socklen_t;
|
addr_len : socklen_t;
|
||||||
reqs := accept(main_socket, *addr, *addr_len);
|
reqs := accept(main_socket, *addr, *addr_len);
|
||||||
@ -129,5 +156,8 @@ main :: () {
|
|||||||
res_string := serialize_response(*res);
|
res_string := serialize_response(*res);
|
||||||
send(reqs, res_string.data, xx res_string.count, 0);
|
send(reqs, res_string.data, xx res_string.count, 0);
|
||||||
close_and_reset(*reqs);
|
close_and_reset(*reqs);
|
||||||
|
// Here we reset the pool, obviously you should not be keeping pointers between requests.
|
||||||
|
reset(*reqpool);
|
||||||
}
|
}
|
||||||
|
close_and_reset(*main_socket);
|
||||||
}
|
}
|
||||||
|
|||||||
2
page.jai
2
page.jai
@ -1,3 +1,5 @@
|
|||||||
|
#load "components/index.jai";
|
||||||
|
|
||||||
serve_page :: (res: *Response, content: string) {
|
serve_page :: (res: *Response, content: string) {
|
||||||
res.body = sprint("%\n%\n%", header, content, footer);
|
res.body = sprint("%\n%\n%", header, content, footer);
|
||||||
}
|
}
|
||||||
|
|||||||
60
pages/index.jai
Normal file
60
pages/index.jai
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
get_main_page :: () -> string {
|
||||||
|
builder : String_Builder;
|
||||||
|
add_navbar(*builder);
|
||||||
|
print_to_builder(*builder, "<div class=\"archive-layout\">\n");
|
||||||
|
add_sidebar(*builder);
|
||||||
|
print_to_builder(*builder, start_grid, state.recipes.count, state.recipes.count);
|
||||||
|
for recipe : state.recipes {
|
||||||
|
print_to_builder(*builder, card, recipe.id, recipe.image, recipe.name, recipe.category, recipe.duration);
|
||||||
|
}
|
||||||
|
print_to_builder(*builder, "%", end_grid);
|
||||||
|
print_to_builder(*builder, "</div>\n");
|
||||||
|
return builder_to_string(*builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
#scope_file
|
||||||
|
|
||||||
|
start_grid : string = #string DONE
|
||||||
|
<main class="main-content">
|
||||||
|
<header class="archive-header">
|
||||||
|
<h2 class="mono" style="font-size: 1rem; color: var(--ink);">// RECIPES</h2>
|
||||||
|
<div class="mono">Showing % of %</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid-dense">
|
||||||
|
DONE
|
||||||
|
|
||||||
|
card : string = #string DONE
|
||||||
|
<a href="/recipe?id=%" class="asset-card">
|
||||||
|
<div class="asset-img"><img src="%" alt="Image of recipe"></div>
|
||||||
|
<h3 class="asset-title">%</h3>
|
||||||
|
<div class="asset-meta mono"><span>%</span> <span>%</span></div>
|
||||||
|
</a>
|
||||||
|
DONE
|
||||||
|
|
||||||
|
end_grid : string = #string DONE
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
DONE
|
||||||
|
|
||||||
|
add_sidebar :: (builder: *String_Builder) {
|
||||||
|
|
||||||
|
categories : Table(string, int);
|
||||||
|
for recipe : state.recipes {
|
||||||
|
categoryptr := table_find_pointer(*categories, recipe.category);
|
||||||
|
if categoryptr {
|
||||||
|
table_set(*categories, recipe.category, categoryptr.* + 1);
|
||||||
|
} else {
|
||||||
|
table_set(*categories, recipe.category, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print_to_builder(builder, "<aside class=\"sidebar\">\n");
|
||||||
|
print_to_builder(builder, "<div class=\"filter-group\">\n");
|
||||||
|
print_to_builder(builder, "<span class=\"filter-title\">Category</span>\n");
|
||||||
|
for count, category : categories {
|
||||||
|
print_to_builder(builder, "<div class=\"filter-option\">% <span class=\"count\">%</span></div>", category, count);
|
||||||
|
}
|
||||||
|
print_to_builder(builder, "</div>\n");
|
||||||
|
print_to_builder(builder, "</aside>\n");
|
||||||
|
}
|
||||||
1
pages/recipe
Normal file
1
pages/recipe
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
22
pages/recipe.jai
Normal file
22
pages/recipe.jai
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
get_recipe_page :: (req: Request) -> string {
|
||||||
|
id := table_find_pointer(*req.query, "id");
|
||||||
|
if !id then return "<h1>Error: invalid recipe id</h1>";
|
||||||
|
|
||||||
|
curRecipe : Recipe;
|
||||||
|
|
||||||
|
for state.recipes if it.id == id.* curRecipe = it;
|
||||||
|
if curRecipe.id == "" then return "<h1>Can't find recipe!</h1>";
|
||||||
|
|
||||||
|
builder : String_Builder;
|
||||||
|
add_navbar(*builder);
|
||||||
|
|
||||||
|
print_to_builder(*builder, recipeHero, curRecipe.image);
|
||||||
|
|
||||||
|
return builder_to_string(*builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeHero : string = #string DONE
|
||||||
|
<div class="recipe-hero">
|
||||||
|
<img id="parallax-img" src="%" alt="Steak">
|
||||||
|
</div>
|
||||||
|
DONE
|
||||||
13
server.jai
13
server.jai
@ -1,10 +1,21 @@
|
|||||||
#load "style.jai";
|
#load "style.jai";
|
||||||
#load "page.jai";
|
#load "page.jai";
|
||||||
|
#load "data.jai";
|
||||||
|
|
||||||
|
#load "pages/index.jai";
|
||||||
|
#load "pages/recipe.jai";
|
||||||
|
|
||||||
|
init_server :: () {
|
||||||
|
init_data();
|
||||||
|
}
|
||||||
|
|
||||||
handler :: (req: Request, res: *Response) {
|
handler :: (req: Request, res: *Response) {
|
||||||
if req.route == {
|
if req.route == {
|
||||||
case "/";
|
case "/";
|
||||||
serve_page(res, "<h1>Hello from Jai!</h1>");
|
serve_page(res, get_main_page());
|
||||||
|
return;
|
||||||
|
case "/recipe";
|
||||||
|
serve_page(res, get_recipe_page(req));
|
||||||
return;
|
return;
|
||||||
case "/style.css";
|
case "/style.css";
|
||||||
serve_stylesheet(res);
|
serve_stylesheet(res);
|
||||||
|
|||||||
121
style.jai
121
style.jai
@ -8,17 +8,120 @@ stylesheet : string = #string DONE
|
|||||||
--ink: #050505;
|
--ink: #050505;
|
||||||
--paper: #FAFAFA;
|
--paper: #FAFAFA;
|
||||||
--gold: #C5A059;
|
--gold: #C5A059;
|
||||||
--gold-dim: #E0D0B0;
|
--gold-dim: #E6DCC8;
|
||||||
|
--grey: #888888;
|
||||||
|
--space: 2rem;
|
||||||
|
}
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&family=JetBrains+Mono:wght@400&family=Playfair+Display:ital,wght@0,300;0,400;0,600;1,400&display=swap');
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { background-color: var(--paper); color: var(--ink); font-family: 'Inter', sans-serif; line-height: 1.6; -webkit-font-smoothing: antialiased; }
|
||||||
|
a { text-decoration: none; color: inherit; }
|
||||||
|
h1, h2, h3 { font-family: 'Playfair Display', serif; font-weight: 400; letter-spacing: -0.02em; }
|
||||||
|
.mono { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--grey); }
|
||||||
|
|
||||||
|
/* --- NAV --- */
|
||||||
|
nav { padding: 1rem var(--space); display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--gold-dim); position: sticky; top: 0; background: rgba(250, 250, 250, 0.98); backdrop-filter: blur(10px); z-index: 100; }
|
||||||
|
.brand { font-family: 'Playfair Display', serif; font-size: 1.2rem; font-weight: 600; letter-spacing: 0.15em; display: flex; align-items: center; }
|
||||||
|
.brand::before { content: ''; display: block; width: 6px; height: 6px; background: var(--gold); margin-right: 16px; transform: rotate(45deg); box-shadow: 0 0 0 2px var(--paper), 0 0 0 3px var(--gold-dim); }
|
||||||
|
.nav-actions a { font-family: 'JetBrains Mono', monospace; text-transform: uppercase; letter-spacing: 0.1em; margin-left: var(--space); font-size: 0.8rem; transition: color 0.2s; }
|
||||||
|
.nav-actions a:hover { color: var(--gold); }
|
||||||
|
.nav-actions a.active { color: var(--gold); border-bottom: 1px solid var(--gold); }
|
||||||
|
|
||||||
|
/* --- LAYOUT: SIDEBAR + GRID --- */
|
||||||
|
.archive-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 240px 1fr; /* Sidebar Fixed Width */
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:wght@452&family=Prata&display=swap');
|
/* Sidebar Styling */
|
||||||
|
.sidebar {
|
||||||
body {
|
padding: 2rem var(--space) 2rem 2rem;
|
||||||
font-family: "Prata", serif;
|
border-right: 1px solid var(--gold-dim);
|
||||||
font-weight: 400;
|
position: sticky;
|
||||||
font-style: normal;
|
top: 60px; /* Below Nav */
|
||||||
background-color: var(--paper);
|
height: calc(100vh - 60px);
|
||||||
color: var(--gold);
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-group { margin-bottom: 2.5rem; }
|
||||||
|
.filter-title { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; font-weight: bold; margin-bottom: 1rem; display: block; color: var(--ink); }
|
||||||
|
|
||||||
|
.filter-option {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
padding: 0.4rem 0; cursor: pointer; font-size: 0.9rem; color: #555; transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.filter-option:hover { color: var(--gold); }
|
||||||
|
.filter-option.selected { color: var(--ink); font-weight: 500; }
|
||||||
|
.filter-option.selected::before { content: '●'; color: var(--gold); font-size: 0.6rem; margin-right: 0.5rem; }
|
||||||
|
|
||||||
|
.count { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: #ccc; }
|
||||||
|
|
||||||
|
/* Dense Grid Styling */
|
||||||
|
.main-content { padding: 2rem; }
|
||||||
|
|
||||||
|
.archive-header {
|
||||||
|
display: flex; justify-content: space-between; align-items: baseline;
|
||||||
|
margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-dense {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); /* Smaller cards */
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-card { cursor: pointer; display: block; }
|
||||||
|
.asset-card:hover .asset-img img { transform: scale(1.05); filter: grayscale(0%); }
|
||||||
|
|
||||||
|
.asset-img {
|
||||||
|
width: 100%; aspect-ratio: 1/1; /* Square for dense packing */
|
||||||
|
background: #eee; overflow: hidden; margin-bottom: 0.8rem;
|
||||||
|
}
|
||||||
|
.asset-img img { width: 100%; height: 100%; object-fit: cover; filter: grayscale(100%); transition: all 0.3s ease; }
|
||||||
|
|
||||||
|
.asset-title { font-size: 1rem; font-weight: 500; line-height: 1.2; margin-bottom: 0.2rem; }
|
||||||
|
.asset-meta { display: flex; gap: 1rem; }
|
||||||
|
|
||||||
|
.recipe-hero { width: 100%; height: 80vh; position: relative; overflow: hidden; z-index: 1; }
|
||||||
|
.recipe-hero img { width: 100%; height: 120%; object-fit: cover; position: absolute; top: 0; left: 0; will-change: transform; }
|
||||||
|
|
||||||
|
.content-wrapper { position: relative; z-index: 10; margin-top: -150px; padding: 0 var(--space); }
|
||||||
|
|
||||||
|
.header-card {
|
||||||
|
background: var(--paper); padding: 3rem var(--space); border: 1px solid var(--gold-dim);
|
||||||
|
max-width: 1200px; margin: 0 auto var(--space) auto; box-shadow: 0 20px 40px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.recipe-title { font-size: 4.5rem; line-height: 1; margin-bottom: 1.5rem; }
|
||||||
|
|
||||||
|
.specs { display: flex; gap: 3rem; margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--gold-dim); }
|
||||||
|
.spec-val { font-family: 'Playfair Display', serif; font-size: 1.5rem; }
|
||||||
|
|
||||||
|
.layout { display: grid; grid-template-columns: 1fr 1.5fr; gap: 5rem; max-width: 1200px; margin: 0 auto; padding-bottom: 100px; }
|
||||||
|
|
||||||
|
.component-row { display: flex; justify-content: space-between; align-items: center; padding: 1.5rem 0; border-bottom: 1px solid var(--gold-dim); }
|
||||||
|
.component-name { font-weight: 500; font-size: 1.1rem; }
|
||||||
|
|
||||||
|
/* Dynamic Slots */
|
||||||
|
.slot-dynamic { background: var(--gold-transparent); padding: 1rem; margin: 1rem -1rem; border-left: 3px solid var(--gold); }
|
||||||
|
.slot-head { display: flex; justify-content: space-between; margin-bottom: 0.5rem; }
|
||||||
|
.slot-tag { color: var(--gold); font-weight: bold; }
|
||||||
|
|
||||||
|
/* HTMX Indicator styles */
|
||||||
|
.htmx-indicator { display: none; }
|
||||||
|
.htmx-request .htmx-indicator { display: inline; }
|
||||||
|
.htmx-request.reroll { animation: spin 0.5s linear infinite; }
|
||||||
|
@keyframes spin { 100% { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
.reroll { background: none; border: 1px solid var(--gold); color: var(--gold); border-radius: 50%; width: 24px; height: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform 0.3s; }
|
||||||
|
.reroll:hover { background: var(--gold); color: var(--paper); }
|
||||||
|
|
||||||
|
.step { margin-bottom: 3rem; display: grid; grid-template-columns: 60px 1fr; }
|
||||||
|
.step-num { font-family: 'Playfair Display', serif; font-size: 2.5rem; color: var(--gold); font-style: italic; line-height: 1; }
|
||||||
|
.step-text { font-size: 1.15rem; font-weight: 300; max-width: 60ch; }
|
||||||
|
|
||||||
|
.edit-fab { position: fixed; bottom: 2rem; right: 2rem; width: 60px; height: 60px; border-radius: 50%; background: var(--ink); color: var(--gold); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 10px 30px rgba(0,0,0,0.2); transition: transform 0.2s; z-index: 99; }
|
||||||
|
.edit-fab:hover { transform: scale(1.1); }
|
||||||
|
|
||||||
DONE
|
DONE
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user