Merge pull request '(FEAT): Wired backend to frontend for recipe creation.' (#7) from feature/recipes into master

Reviewed-on: #7
This commit is contained in:
Hayden Hargreaves 2025-07-01 20:04:17 -07:00
commit d183d9d38f
10 changed files with 384 additions and 146 deletions

View File

@ -135,50 +135,50 @@ creation process will take place here
##### UI Requirements
- [ ] Create Recipe Heading Banner
- [ ] Large, simple text banner that displays a header
- [ ] Smaller text section that contains some useful information for using the wizard
- [x] Create Recipe Heading Banner
- [x] Large, simple text banner that displays a header
- [x] Smaller text section that contains some useful information for using the wizard
- [ ] Recipe Creation Wizard
- [ ] **Recipe Details:** required
- [ ] **Recipe title:** small text input (1 to 128)
- [ ] **Recipe description:** large text input
- [ ] **Meal category:** pill radio buttons or drop down*
- [ ] **Service size:** numeric input (1 to 16)
- [ ] **Difficulty rating:** star buttons that can be selected (up to 5)
- [ ] **Duration:** prep and cook time, individual numeric inputs
- [ ] **Ingredients:** required
- [ ] **Dynamic ingredient list:** List of each ingredient
- [ ] **Content:** name and quantity (both text inputs)
- [x] Recipe Creation Wizard
- [x] **Recipe Details:** required
- [x] **Recipe title:** small text input (1 to 128)
- [x] **Recipe description:** large text input
- [x] **Meal category:** pill radio buttons or drop down*
- [x] **Service size:** numeric input (1 to 16)
- [x] **Difficulty rating:** star buttons that can be selected (up to 5)
- [x] **Duration:** prep and cook time, individual numeric inputs
- [x] **Ingredients:** required
- [x] **Dynamic ingredient list:** List of each ingredient
- [x] **Content:** name and quantity (both text inputs)
- [ ] **Actions:** delete button and **reorder element***
- [ ] **Add ingredient button:** Simple button to add a blank ingredient row
- [ ] **Instructions:** required
- [ ] **Dynamic instructions list:** Numbered list of each instruction
- [ ] **Content:** number and large text instruction
- [x] **Add ingredient button:** Simple button to add a blank ingredient row
- [x] **Instructions:** required
- [x] **Dynamic instructions list:** Numbered list of each instruction
- [x] **Content:** number and large text instruction
- [ ] **Rich text editor?***
- [ ] **Actions:** delete button and **reorder element***
- [ ] **Add step button:** Simple button to add a blank instruction element
- [ ] **Media & Tags:** optional
- [ ] **Image Upload**
- [ ] Single image selector for the thumbnail image
- [ ] Small image display once one has been upload
- [ ] Remove button to remove the image
- [ ] Replace button to replace the image
- [ ] **Tags**
- [ ] Text input to add tags
- [x] **Add step button:** Simple button to add a blank instruction element
- [x] **Media & Tags:** optional
- [x] **Image Upload**
- [x] Single image selector for the thumbnail image
- [x] Small image display once one has been upload
- [x] Remove button to remove the image
- [x] Replace button to replace the image
- [x] **Tags**
- [x] Text input to add tags
- [ ] Using list of existing tags, use a prefill while typing
- [ ] Tags that don't exist will be added
- [ ] Display a small list of added tags
- [ ] Clicking a tag will remove it from the list
- [ ] **Footer & Submit**
- [ ] Save recipe button to complete the form
- [x] Tags that don't exist will be added
- [x] Display a small list of added tags
- [x] Clicking a tag will remove it from the list
- [x] **Footer & Submit**
- [x] Save recipe button to complete the form
- [ ] Button is disabled until the minimum required fields are complete*
- [ ] Input Validation
- [ ] Required elements should have a **required indicator**
- [ ] Required elements will be validated input
- [x] Input Validation
- [x] Required elements should have a **required indicator**
- [x] Required elements will be validated input
- [ ] **Valid:** Green outline or indicator* maybe a small, green check mark
- [ ] **Invalid:** Red outline or indicator* maybe a red border with error text
- [x] **Invalid:** Red outline or indicator* maybe a red border with error text
'*': Not sure yet, still under consideration
@ -187,13 +187,13 @@ creation process will take place here
##### API Requirements
- [ ] Middleware
- [ ] **Authentication:** User must be logged in to access this page
- [ ] **Authentication:** User must be logged in to submit the creation of a recipe (fallback)
- [x] Middleware
- [x] **Authentication:** User must be logged in to access this page
- [x] **Authentication:** User must be logged in to submit the creation of a recipe (fallback)
- [ ] Recipe Creation Wizard
- [ ] Create a new recipe object in the database
- [ ] Recipe should be attached to a user (logged in)
- [x] Recipe Creation Wizard
- [x] Create a new recipe object in the database
- [x] Recipe should be attached to a user (logged in)
- [ ] User should be directed to the view recipe page on a successful creation

View File

@ -31,6 +31,12 @@ func FavoritesPage(ctx *gin.Context) {
}
func CreatePage(ctx *gin.Context) {
// If not logged in, direct to the login page
if !domain.IsLoggedIn(ctx) {
ctx.Redirect(http.StatusSeeOther, domain.WEB_LOGIN)
return
}
title := "Potion - Create"
page := pages.CreatePage()

View File

@ -4,46 +4,27 @@ import (
"net/http"
"github.com/gin-gonic/gin"
// domain "github.com/haydenhargreaves/Potion/internal/domain/server"
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
)
const CREATE_SUCCESS_HTML = `
<p id="response" class="text-sm text-green-600 px-4 py-1 bg-green-100 rounded-full w-fit">
Success! Your new masterpiece was created!
</p>
`
const CREATE_ERROR_HTML = `
<p id="response" class="text-sm text-red-500 px-4 py-1 bg-red-100 rounded-full w-fit">
Uh oh! Something went wrong when creating your recipe. Please try again. %s
</p>
`
func CreateRecipe(ctx *gin.Context) {
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
title := ctx.PostForm("title")
description := ctx.PostForm("description")
preparation := ctx.PostForm("preparation-time")
cook := ctx.PostForm("cook-time")
serving := ctx.PostForm("serving-size")
category := ctx.PostForm("category")
difficulty := ctx.PostForm("difficulty")
ingredients := ctx.PostFormArray("ingredients")
quantity := ctx.PostFormArray("quantity")
instructions := ctx.PostFormArray("instructions")
tags := ctx.PostForm("tags") // this is a list of strings split with a comma (,)
// Have to get the image differently
image, err := ctx.FormFile("image")
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
_, err := deps.RecipeService.CreateRecipe(ctx)
if err != nil {
ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
ctx.String(http.StatusOK, CREATE_ERROR_HTML, err.Error())
return
}
ctx.JSON(http.StatusOK, gin.H{
"title": title,
"description": description,
"cook time": cook,
"preparation time": preparation,
"serving size": serving,
"category": category,
"difficulty": difficulty,
"ingredients": ingredients,
"quantity": quantity,
"instructions": instructions,
"tags": tags,
"image": image.Filename,
})
// deps.RecipeService.CreateRecipe(ctx)
// ctx.JSON(http.StatusCreated, gin.H{"recipe": recipe})
ctx.String(http.StatusOK, CREATE_SUCCESS_HTML)
}

View File

@ -1,11 +1,16 @@
package service
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
)
// RecipeService implements the domain.RecipeService defined in the domain module.
@ -22,45 +27,85 @@ func NewRecipeService(recipeRepository domain.RecipeRepository) domain.RecipeSer
return &RecipeService{recipeRepository: recipeRepository}
}
func (s *RecipeService) CreateRecipe(ctx *gin.Context) domain.Recipe {
// TODO: Implement
func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
// Ensure user is logged in
if !domainServer.IsLoggedIn(ctx) {
return nil, fmt.Errorf("User is not logged in.")
}
title := ctx.PostForm("title")
description := ctx.PostForm("description")
preparation := ctx.PostForm("preparation-time")
cook := ctx.PostForm("cook-time")
serving := ctx.PostForm("serving-size")
category := ctx.PostForm("category")
difficulty := ctx.PostForm("difficulty")
ingredients := ctx.PostFormArray("ingredients")
quantity := ctx.PostFormArray("quantity")
instructions := ctx.PostFormArray("instructions")
tags := strings.Split(ctx.PostForm("tags"), ",")
userId := ctx.MustGet("userId").(int)
// Have to get the image differently
image, err := ctx.FormFile("image")
if err != nil && !errors.Is(err, http.ErrMissingFile) {
// Error getting image
}
// Convert to proper values
servingInt, _ := strconv.Atoi(serving)
difficultyInt, _ := strconv.Atoi(difficulty)
prepInt, _ := strconv.Atoi(preparation)
cookInt, _ := strconv.Atoi(cook)
var ingredientSlice []domain.RecipeIngredient
for i := range len(ingredients) {
if strings.TrimSpace(ingredients[i]) != "" {
ins := domain.RecipeIngredient{
Name: ingredients[i],
Quantity: quantity[i],
}
ingredientSlice = append(ingredientSlice, ins)
}
}
var instructionSlice []string
for _, ins := range instructions {
if ins != "" {
instructionSlice = append(instructionSlice, ins)
}
}
// Create the recipe
recipe := domain.Recipe{
Title: "Delicious Go Curry",
Description: "A savory and easy-to-make curry, perfect for weeknights.",
Instructions: []string{
"Chop all vegetables.",
"Sauté onions and garlic until fragrant.",
"Add curry paste and stir for 1 minute.",
"Add coconut milk and vegetables, simmer until cooked.",
"Serve with rice.",
},
Serves: 4,
Difficulty: 3,
Title: title,
Description: description,
Instructions: instructionSlice,
Serves: servingInt,
Difficulty: difficultyInt,
Duration: domain.RecipeDuration{
Total: 45,
Prep: 15,
Cook: 30,
Total: prepInt + cookInt,
Prep: prepInt,
Cook: cookInt,
},
Category: domain.MealDinner, // Using our EMeal type. Ensure this matches an updated enum value.
Ingredients: []domain.RecipeIngredient{
{Name: "Onion", Quantity: "1 large"},
{Name: "Garlic", Quantity: "3 cloves"},
{Name: "Curry Paste", Quantity: "2 tbsp"},
{Name: "Coconut Milk", Quantity: "400ml can"},
{Name: "Broccoli", Quantity: "1 head"},
{Name: "Bell Pepper", Quantity: "1 red"},
{Name: "Rice", Quantity: "As needed"},
},
UserId: 3,
Created: time.Now(),
Category: domain.RecipeMeal(category),
Ingredients: ingredientSlice,
UserId: userId,
Created: time.Now(),
}
if err := s.recipeRepository.CreateRecipe(&recipe); err != nil {
ctx.JSON(http.StatusOK, gin.H{"err": err.Error()})
return domain.Recipe{}
return &recipe, err
}
ctx.JSON(http.StatusCreated, gin.H{"recipe": recipe})
return recipe
// TODO: Upload the image
if image != nil {
}
// TODO: Create the tags in the database
if len(tags) > 0 {
}
return &recipe, nil
}

View File

@ -3,6 +3,5 @@ package domain
import "github.com/gin-gonic/gin"
type RecipeService interface {
CreateRecipe(ctx *gin.Context) Recipe
CreateRecipe(ctx *gin.Context) (*Recipe, error)
}

View File

@ -13,7 +13,6 @@ templ CreatePage() {
templ Page() {
@components.BannerText("Create Your Masterpiece")
<p id="output"></p>
<div class="mx-4 md:mx-16 my-8">
<p class="mb-8">
Welcome to the Recipe Creation Wizard! Simply fill in the details about your culinary creation,
@ -24,7 +23,13 @@ templ Page() {
button to
share your masterpiece!
</p>
<form>
<form
hx-post="/v1/api/recipe"
hx-swap="outerHTML"
hx-target="#response"
hx-trigger="submit"
hx-encoding="multipart/form-data"
>
<div class="flex flex-col">
<label for="title" class="text-sm mb-2">
Recipe Title
@ -32,12 +37,17 @@ templ Page() {
</label>
<input
onkeydown="return event.key != 'Enter';"
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
type="text"
id="title"
name="title"
required
maxlength="128"
minlength="1"
placeholder="e.g., Classic Chicken Curry"
/>
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">Please enter a title. Between 1-128 characters.</p>
</div>
<div class="flex flex-col my-4">
<label for="description" class="text-sm mb-2">
@ -45,12 +55,19 @@ templ Page() {
<span class="text-red-500">*</span>
</label>
<textarea
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm"
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm invalid:border-red-500"
id="description"
name="description"
rows="4"
required
maxlength="1024"
minlength="1"
placeholder="A brief description of your delicious recipe..."
></textarea>
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter a description. Between 1-1000 characters.
</p>
</div>
<div class="my-4 flex flex-col gap-x-2">
<div class="flex flex-col flex-grow">
@ -84,12 +101,19 @@ templ Page() {
</label>
<input
onkeydown="return event.key != 'Enter';"
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
type="number"
id="preparation-time"
name="preparation-time"
required
min="0"
max="120"
placeholder="e.g., 20"
/>
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter a time (minutes).
</p>
</div>
<div class="flex flex-col flex-grow w-1/3">
<label for="cook-time" class="text-sm mb-2">
@ -98,12 +122,19 @@ templ Page() {
</label>
<input
onkeydown="return event.key != 'Enter';"
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
type="number"
id="cook-time"
name="cook-time"
required
min="0"
max="120"
placeholder="e.g., 45"
/>
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter a time (minutes).
</p>
</div>
<div class="flex flex-col flex-grow w-1/3">
<label for="serving-size" class="text-sm mb-2">
@ -112,14 +143,19 @@ templ Page() {
</label>
<input
onkeydown="return event.key != 'Enter';"
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
type="number"
max="16"
min="1"
required
id="serving-size"
name="serving-size"
placeholder="e.g., 4"
/>
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter a serving size.
</p>
</div>
</div>
<div class="my-4 flex gap-x-2">
@ -131,7 +167,10 @@ templ Page() {
<select
id="category"
name="category"
class="border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
required
class="peer border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
invalid:border-red-500"
>
<option value="">Select a category</option>
<option value="breakfast">Breakfast</option>
@ -142,6 +181,9 @@ templ Page() {
<option value="side">Side</option>
<option value="other">Other</option>
</select>
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
Please select a category.
</p>
</div>
<div class="flex flex-col flex-grow w-1/3">
<label for="difficulty" class="text-sm mb-2">
@ -151,7 +193,10 @@ templ Page() {
<select
id="difficulty"
name="difficulty"
class="border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
required
class="peer border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
invalid:border-red-500"
>
<option value="">Select a difficulty</option>
<option value="1">Beginner</option>
@ -160,6 +205,9 @@ templ Page() {
<option value="4">Challenging</option>
<option value="5">Extreme</option>
</select>
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
Please select a difficulty.
</p>
</div>
</div>
<div class="flex flex-col my-4">
@ -169,22 +217,40 @@ templ Page() {
</label>
<div id="ingredient-list">
<div class="w-full flex gap-x-2 py-2">
<input
onkeydown="return event.key != 'Enter';"
class="flex-grow border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
type="text"
id="ingredients"
name="ingredients"
placeholder="Ingredient name (e.g., Chicken Breast)"
/>
<input
onkeydown="return event.key != 'Enter';"
class="w-1/3 border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
type="text"
id="quantity"
name="quantity"
placeholder="Quantity (e.g., 1lb)"
/>
<div class="flex-grow">
<input
onkeydown="return event.key != 'Enter';"
class="peer w-full border border-gray-300 px-4 py-2 rounded-lg focus:outline-none
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
invalid:border-red-500"
type="text"
id="ingredients"
name="ingredients"
required
minlength="1"
placeholder="Ingredient name (e.g., Chicken Breast)"
/>
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter at least one ingredient.
</p>
</div>
<div class="w-1/3">
<input
onkeydown="return event.key != 'Enter';"
class="peer w-full border border-gray-300 px-4 py-2 rounded-lg focus:outline-none
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
invalid:border-red-500"
type="text"
id="quantity"
name="quantity"
required
minlength="1"
placeholder="Quantity (e.g., 1lb)"
/>
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
Please provide a quantity.
</p>
</div>
</div>
</div>
<button
@ -202,12 +268,19 @@ templ Page() {
</label>
<div id="instruction-list" class="flex flex-col">
<textarea
class="border border-gray-300 my-2 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm"
class="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm invalid:border-red-500
valid:my-2 invalid:mt-2"
id="instructions"
name="instructions"
rows="3"
required
minlength="1"
placeholder="Step 1: Describe this step..."
></textarea>
<p class="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter at least one step.
</p>
</div>
<button
type="button"
@ -229,14 +302,9 @@ templ Page() {
class="my-2 block w-full text-sm text-placeholder file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:bg-blue-100 file:text-blue-700 cursor-pointer"
/>
</div>
<!-- <form hx-post="/v1/api/recipe" hx-swap="innerHTML" hx-target="#output" hx-trigger="submit"> -->
<p id="response" class="hidden"></p>
<button
type="submit"
hx-post="/v1/api/recipe"
hx-swap="innerHTML"
hx-target="#output"
hx-trigger="click"
hx-encoding="multipart/form-data"
class="w-full mt-8 bg-gradient-to-r from-blue-200 to-purple-200 py-2 rounded-lg text-lg cursor-pointer shadow-md"
>
Create Recipe
@ -271,7 +339,7 @@ templ Page() {
function addInstruction() {
const list = document.getElementById("instruction-list");
const itemNum = list.children.length + 1;
const itemNum = list.querySelectorAll("textarea").length + 1;
const item = document.createElement("textarea");
item.id = "instructions";
item.name = "instructions";

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
package templates
templ RecipePage() {
<p>
Viewing page
</p>
}

View File

@ -0,0 +1,40 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.865
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func RecipePage() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<p>Viewing page</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -9,6 +9,10 @@
monospace;
--color-red-100: oklch(93.6% 0.032 17.717);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-green-100: oklch(96.2% 0.044 156.743);
--color-green-500: oklch(72.3% 0.219 149.579);
--color-green-600: oklch(62.7% 0.194 149.214);
--color-green-700: oklch(52.7% 0.154 150.069);
--color-blue-100: oklch(93.2% 0.032 255.585);
--color-blue-200: oklch(88.2% 0.059 254.128);
--color-blue-300: oklch(80.9% 0.105 251.813);
@ -215,6 +219,9 @@
.static {
position: static;
}
.top-1 {
top: calc(var(--spacing) * 1);
}
.top-1\/2 {
top: calc(1/2 * 100%);
}
@ -224,6 +231,9 @@
.left-0 {
left: calc(var(--spacing) * 0);
}
.left-1 {
left: calc(var(--spacing) * 1);
}
.left-1\/2 {
left: calc(1/2 * 100%);
}
@ -242,6 +252,9 @@
.mx-4 {
margin-inline: calc(var(--spacing) * 4);
}
.my-1 {
margin-block: calc(var(--spacing) * 1);
}
.my-2 {
margin-block: calc(var(--spacing) * 2);
}
@ -338,9 +351,15 @@
.h-screen {
height: 100vh;
}
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-1\/3 {
width: calc(1/3 * 100%);
}
.w-3 {
width: calc(var(--spacing) * 3);
}
.w-3\/4 {
width: calc(3/4 * 100%);
}
@ -353,6 +372,9 @@
.w-5 {
width: calc(var(--spacing) * 5);
}
.w-9 {
width: calc(var(--spacing) * 9);
}
.w-9\/10 {
width: calc(9/10 * 100%);
}
@ -374,16 +396,30 @@
.max-w-xl {
max-width: var(--container-xl);
}
.flex-shrink {
flex-shrink: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
.border-collapse {
border-collapse: collapse;
}
.-translate-x-1 {
--tw-translate-x: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-x-1\/2 {
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1 {
--tw-translate-y: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1\/2 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
@ -391,6 +427,9 @@
.cursor-pointer {
cursor: pointer;
}
.resize {
resize: both;
}
.resize-none {
resize: none;
}
@ -509,6 +548,12 @@
.bg-gray-200 {
background-color: var(--color-gray-200);
}
.bg-green-100 {
background-color: var(--color-green-100);
}
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-white {
background-color: var(--color-white);
}
@ -671,6 +716,15 @@
.text-gray-800 {
color: var(--color-gray-800);
}
.text-green-500 {
color: var(--color-green-500);
}
.text-green-600 {
color: var(--color-green-600);
}
.text-green-700 {
color: var(--color-green-700);
}
.text-red-500 {
color: var(--color-red-500);
}
@ -680,6 +734,9 @@
.uppercase {
text-transform: uppercase;
}
.underline {
text-decoration-line: underline;
}
.shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@ -758,6 +815,11 @@
.\[-webkit-line-clamp\:4\] {
-webkit-line-clamp: 4;
}
.peer-invalid\:block {
&:is(:where(.peer):invalid ~ *) {
display: block;
}
}
.file\:mr-4 {
&::file-selector-button {
margin-right: calc(var(--spacing) * 4);
@ -800,6 +862,26 @@
color: var(--color-blue-700);
}
}
.valid\:my-2 {
&:valid {
margin-block: calc(var(--spacing) * 2);
}
}
.valid\:border-gray-300 {
&:valid {
border-color: var(--color-gray-300);
}
}
.invalid\:mt-2 {
&:invalid {
margin-top: calc(var(--spacing) * 2);
}
}
.invalid\:border-red-500 {
&:invalid {
border-color: var(--color-red-500);
}
}
.hover\:cursor-pointer {
&:hover {
@media (hover: hover) {
@ -1054,6 +1136,16 @@
width: calc(2/7 * 100%);
}
}
.\[\&\:not\(\:placeholder-shown\)\:invalid\]\:border-red-500 {
&:not(:placeholder-shown):invalid {
border-color: var(--color-red-500);
}
}
.\[\&\:not\(\:placeholder-shown\)\:valid\]\:border-green-500 {
&:not(:placeholder-shown):valid {
border-color: var(--color-green-500);
}
}
}
@property --tw-translate-x {
syntax: "*";