27 Commits

Author SHA1 Message Date
Priec
e51eda9a8c default unchecked
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
2026-06-19 11:39:40 +02:00
Priec
12e00a782d profile name surname and save profile data 2026-06-19 11:37:51 +02:00
Priec
5278988842 registration password match 2026-06-19 11:19:30 +02:00
Priec
e70743996b register form fields 2026-06-19 11:14:47 +02:00
Priec
11762728c9 mail sent over test working fully now
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
2026-06-19 10:54:30 +02:00
Priec
ebb208baba not working smtp test 2026-06-19 10:40:43 +02:00
Priec
7cba3d9eba resend verification mail 2026-06-19 01:05:18 +02:00
Priec
35e2b6edc9 hide .env credentials
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
2026-06-19 00:35:48 +02:00
Priec
f3daa27ce7 account type is permanent and password registration is now working at checkout
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
2026-06-18 22:10:17 +02:00
Priec
46cc2459bd required
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
2026-06-18 21:38:32 +02:00
Priec
996358be87 company or personal
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
2026-06-18 21:27:15 +02:00
Priec
c6624e1b3d profile of a new registered users 2026-06-18 21:11:48 +02:00
Priec
b9c1277876 upgrades that are harmless 2026-06-18 20:38:47 +02:00
Priec
42bab82960 oauth2 2026-06-18 18:26:40 +02:00
Priec
7da4109584 RBAC via casbin
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
2026-06-18 18:02:44 +02:00
Priec
ed607e3d27 login register 2026-06-18 17:19:04 +02:00
Priec
7af0a48e92 last penguin ui port
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
2026-06-18 16:42:51 +02:00
Priec
1cd2b86b74 theme switch handled by the penguin ui 2026-06-18 16:25:30 +02:00
Priec
68381d558a navbar is now penguinui 2026-06-18 16:06:51 +02:00
Priec
36a5e7c5fc porting to the use of penguinui5 2026-06-18 11:45:39 +02:00
Priec
e8c6035eeb porting to the use of penguinui 2026-06-18 11:08:10 +02:00
Priec
9a3c68eae5 port of the new penguinui placing 2026-06-18 10:51:07 +02:00
Priec
ee944ed5ce penguinui is now in the root dir in penguinui-components/ 2026-06-18 10:21:29 +02:00
Priec
0a619517b6 move to penguinui continues3
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
2026-06-18 00:54:30 +02:00
Priec
1538d870b9 move to penguinui continues 2026-06-18 00:20:39 +02:00
Priec
ed2eb036ae still not fully migrated to penguinui 2026-06-17 22:52:20 +02:00
Priec
ae99ec079f this broke nice UI for more clear usage of penguin ui 2026-06-17 22:52:07 +02:00
267 changed files with 12608 additions and 1072 deletions

2
.gitignore vendored
View File

@@ -19,6 +19,8 @@ target/
*.sqlite-* *.sqlite-*
.env .env
.env.production .env.production
.envrc
.direnv/
uploads/ uploads/
*.report.html *.report.html
favicon_io.zip favicon_io.zip

1599
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
[package] [package]
name = "kompress_eshop" name = "kompress_eshop"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
publish = false publish = false
default-run = "kompress-eshop-cli" default-run = "kompress-eshop-cli"
@@ -16,14 +16,14 @@ loco-rs = { version = "0.16" }
loco-rs = { workspace = true } loco-rs = { workspace = true }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" } serde_json = { version = "1" }
tokio = { version = "1.45", default-features = false, features = [ tokio = { version = "1.52", default-features = false, features = [
"rt-multi-thread", "rt-multi-thread",
] } ] }
async-trait = { version = "0.1" } async-trait = { version = "0.1" }
axum = { version = "0.8", features = ["multipart"] } axum = { version = "0.8", features = ["multipart"] }
tracing = { version = "0.1" } tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
regex = { version = "1.11" } regex = { version = "1.12" }
migration = { path = "migration" } migration = { path = "migration" }
sea-orm = { version = "1.1", features = [ sea-orm = { version = "1.1", features = [
"sqlx-sqlite", "sqlx-sqlite",
@@ -35,7 +35,7 @@ chrono = { version = "0.4" }
time = { version = "0.3" } time = { version = "0.3" }
dotenvy = { version = "0.15" } dotenvy = { version = "0.15" }
validator = { version = "0.20" } validator = { version = "0.20" }
uuid = { version = "1.6", features = ["v4"] } uuid = { version = "1.23", features = ["v4"] }
include_dir = { version = "0.7" } include_dir = { version = "0.7" }
# outbound HTTP for carrier shipment APIs (Packeta / DPD / DHL) # outbound HTTP for carrier shipment APIs (Packeta / DPD / DHL)
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
@@ -45,6 +45,10 @@ unic-langid = { version = "0.9" }
# /view engine # /view engine
axum-extra = { version = "0.10", features = ["form"] } axum-extra = { version = "0.10", features = ["form"] }
bytes = { version = "1" } bytes = { version = "1" }
axum-casbin = "1.3.0"
loco-oauth2 = "0.5.0"
passwords = "3.1.16"
tower-sessions = "0.14"
[[bin]] [[bin]]
name = "kompress-eshop-cli" name = "kompress-eshop-cli"
@@ -53,6 +57,6 @@ required-features = []
[dev-dependencies] [dev-dependencies]
loco-rs = { workspace = true, features = ["testing"] } loco-rs = { workspace = true, features = ["testing"] }
serial_test = { version = "3.1.1" } serial_test = { version = "3.5.0" }
rstest = { version = "0.25" } rstest = { version = "0.25" }
insta = { version = "1.34", features = ["redactions", "yaml", "filters"] } insta = { version = "1.48", features = ["redactions", "yaml", "filters"] }

View File

@@ -18,6 +18,12 @@
/* Scan every template so used utility classes are emitted. */ /* Scan every template so used utility classes are emitted. */
@source "../views"; @source "../views";
/* penguinui-components/ is the read-only vendored PenguinUI library
* (reference only — never {% include %}'d, never edited). Tailwind v4
* auto-detects it from the project root, so exclude it explicitly or
* its 177 files bloat the build with classes we never render. */
@source not "../../penguinui-components";
/* PenguinUI toggles dark styles with a `dark:` variant. This app /* PenguinUI toggles dark styles with a `dark:` variant. This app
* already sets <html data-theme="dark|light"> (see base.html), so * already sets <html data-theme="dark|light"> (see base.html), so
* key the variant off that attribute instead of the OS setting. */ * key the variant off that attribute instead of the OS setting. */

View File

@@ -57,12 +57,31 @@ album-by = by
album-play-full = Play full album album-play-full = Play full album
album-queue-all = queue all tracks in order album-queue-all = queue all tracks in order
album-no-tracks = no tracks yet album-no-tracks = no tracks yet
login-title = Admin login login-title = Sign in
login-error = Access denied - invalid email or password. login-error = Access denied - invalid email or password.
login-error-unverified = Your account isn't verified yet. Check your email and click the verification link.
login-root = root login-root = root
login-auth = Authenticate login-auth = Sign in
login-email = Email login-email = Email
login-password = Password login-password = Password
login-no-account = Don't have an account?
login-have-account = Already have an account?
auth-or = or
auth-google = Continue with Google
nav-login = Sign in
nav-register = Register
nav-profile = My profile
register-title = Create account
register-name = Name
register-submit = Create account
register-error-exists = An account with this email already exists.
register-error-invalid = Please check the details you entered and try again.
verify-sent-title = Check your email
verify-sent-body = We've sent a verification link to
verify-ok-title = Account verified
verify-ok-body = Your account is verified. You can now sign in.
verify-fail-title = Verification failed
verify-fail-body = This link is invalid or has expired.
auth = Auth auth = Auth
admin-session = Session admin-session = Session
readonly = readonly readonly = readonly
@@ -205,6 +224,8 @@ add-to-cart = Add to cart
cart-added = Added to cart cart-added = Added to cart
in-stock = In stock in-stock = In stock
out-of-stock = Out of stock out-of-stock = Out of stock
gallery-prev = Previous image
gallery-next = Next image
confirm-delete = Delete this for good? confirm-delete = Delete this for good?
shop-title = Shop shop-title = Shop
shop-subtitle = browse our products. shop-subtitle = browse our products.
@@ -236,8 +257,45 @@ country-de = Germany
country-pl = Poland country-pl = Poland
country-hu = Hungary country-hu = Hungary
checkout-note = Order note checkout-note = Order note
checkout-save-profile = Save this address to my profile
account-type = Account type
account-personal = Individual
account-company = Company
account-company-details = Company details
company-name = Company name
company-ico = Company ID (IČO)
company-dic = Tax ID (DIČ)
company-icdph = VAT ID (IČ DPH)
field-optional = optional
checkout-place-order = Place order checkout-place-order = Place order
checkout-summary = Order summary checkout-summary = Order summary
profile-title = My profile
profile-intro = We'll use these details to prefill checkout.
profile-saved = Profile saved.
profile-save = Save profile
profile-company-required = For a company account, please fill in company name, IČO and DIČ.
profile-first-name = First name
profile-last-name = Surname
profile-edit = Edit profile
profile-cancel = Cancel
profile-not-set = Not set
account-type-locked = Account type can't be changed after registration.
checkout-create-account = Create an account from this order
checkout-create-account-hint = We'll email you a link to set your password. This order will be linked to your account.
order-account-created = We created an account for you. Check your email to set your password.
set-password-title = Set your password
set-password-intro = Choose a password to finish setting up your account.
set-password-new = New password
set-password-confirm = Confirm password
set-password-submit = Set password
set-password-invalid = This link is invalid or has expired.
set-password-weak = Password must be at least 8 characters.
set-password-mismatch = Passwords don't match.
resend-verification-title = Resend verification email
resend-verification-intro = Enter your email and we'll send a fresh verification link.
resend-verification-submit = Resend
resend-verification-done = If that email belongs to an unverified account, we've sent a new verification link. Check your inbox (and spam). You can request another in a minute.
login-resend = Didn't get the verification email? Resend it
order-confirmed-title = Thank you for your order! order-confirmed-title = Thank you for your order!
order-confirmed-sub = We have received your order. order-confirmed-sub = We have received your order.
order-number = Order number order-number = Order number

View File

@@ -57,12 +57,31 @@ album-by = od
album-play-full = Prehrať celý album album-play-full = Prehrať celý album
album-queue-all = zoradiť všetky skladby v poradí album-queue-all = zoradiť všetky skladby v poradí
album-no-tracks = zatiaľ žiadne skladby album-no-tracks = zatiaľ žiadne skladby
login-title = Prihlásenie admina login-title = Prihlásenie
login-error = Prístup odmietnutý - nesprávny e-mail alebo heslo. login-error = Prístup odmietnutý - nesprávny e-mail alebo heslo.
login-error-unverified = Účet ešte nie je overený. Skontrolujte si e-mail a kliknite na overovací odkaz.
login-root = root login-root = root
login-auth = Prihlásiť sa login-auth = Prihlásiť sa
login-email = E-mail login-email = E-mail
login-password = Heslo login-password = Heslo
login-no-account = Nemáte účet?
login-have-account = Už máte účet?
auth-or = alebo
auth-google = Pokračovať cez Google
nav-login = Prihlásiť sa
nav-register = Registrácia
nav-profile = Môj profil
register-title = Vytvoriť účet
register-name = Meno
register-submit = Zaregistrovať sa
register-error-exists = Účet s týmto e-mailom už existuje.
register-error-invalid = Skontrolujte zadané údaje a skúste to znova.
verify-sent-title = Skontrolujte si e-mail
verify-sent-body = Poslali sme overovací odkaz na adresu
verify-ok-title = Účet overený
verify-ok-body = Váš účet je overený. Teraz sa môžete prihlásiť.
verify-fail-title = Overenie zlyhalo
verify-fail-body = Tento odkaz je neplatný alebo mu vypršala platnosť.
auth = Overenie auth = Overenie
admin-session = Relácia admin-session = Relácia
readonly = iba na čítanie readonly = iba na čítanie
@@ -205,6 +224,8 @@ add-to-cart = Pridať do košíka
cart-added = Pridané do košíka cart-added = Pridané do košíka
in-stock = Na sklade in-stock = Na sklade
out-of-stock = Vypredané out-of-stock = Vypredané
gallery-prev = Predchádzajúci obrázok
gallery-next = Ďalší obrázok
confirm-delete = Naozaj zmazať? confirm-delete = Naozaj zmazať?
shop-title = Obchod shop-title = Obchod
shop-subtitle = prezrite si našu ponuku produktov. shop-subtitle = prezrite si našu ponuku produktov.
@@ -236,8 +257,45 @@ country-de = Nemecko
country-pl = Poľsko country-pl = Poľsko
country-hu = Maďarsko country-hu = Maďarsko
checkout-note = Poznámka k objednávke checkout-note = Poznámka k objednávke
checkout-save-profile = Uložiť túto adresu do môjho profilu
account-type = Typ účtu
account-personal = Súkromná osoba
account-company = Firma
account-company-details = Firemné údaje
company-name = Názov firmy
company-ico = IČO
company-dic = DIČ
company-icdph = IČ DPH
field-optional = nepovinné
checkout-place-order = Odoslať objednávku checkout-place-order = Odoslať objednávku
checkout-summary = Súhrn objednávky checkout-summary = Súhrn objednávky
profile-title = Môj profil
profile-intro = Tieto údaje použijeme na predvyplnenie pokladne.
profile-saved = Profil bol uložený.
profile-save = Uložiť profil
profile-company-required = Pri firemnom účte vyplňte názov firmy, IČO a DIČ.
profile-first-name = Meno
profile-last-name = Priezvisko
profile-edit = Upraviť profil
profile-cancel = Zrušiť
profile-not-set = Neuvedené
account-type-locked = Typ účtu sa po registrácii nedá zmeniť.
checkout-create-account = Vytvoriť účet z tejto objednávky
checkout-create-account-hint = Pošleme vám e-mail na nastavenie hesla. Objednávka sa priradí k vášmu účtu.
order-account-created = Vytvorili sme vám účet. Skontrolujte si e-mail a nastavte si heslo.
set-password-title = Nastavte si heslo
set-password-intro = Zvoľte si heslo a dokončite vytvorenie účtu.
set-password-new = Nové heslo
set-password-confirm = Potvrďte heslo
set-password-submit = Nastaviť heslo
set-password-invalid = Odkaz je neplatný alebo vypršal.
set-password-weak = Heslo musí mať aspoň 8 znakov.
set-password-mismatch = Heslá sa nezhodujú.
resend-verification-title = Znova odoslať overovací e-mail
resend-verification-intro = Zadajte svoj e-mail a pošleme vám nový overovací odkaz.
resend-verification-submit = Odoslať znova
resend-verification-done = Ak k tomuto e-mailu patrí neoverený účet, poslali sme naň nový overovací odkaz. Skontrolujte si schránku aj priečinok so spamom. Ďalšiu žiadosť môžete odoslať o minútu.
login-resend = Nedostali ste overovací e-mail? Poslať znova
order-confirmed-title = Ďakujeme za objednávku! order-confirmed-title = Ďakujeme za objednávku!
order-confirmed-sub = Vašu objednávku sme prijali. order-confirmed-sub = Vašu objednávku sme prijali.
order-number = Číslo objednávky order-number = Číslo objednávky

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,228 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="profile-title", lang=lang | default(value='sk')) }}{% endblock title %}
{% macro field(label, value) %}
<div class="space-y-1.5">
<label class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ label }}</label>
{% if value %}
<p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">{{ value }}</p>
{% else %}
<p class="text-sm italic text-on-surface/50 dark:text-on-surface-dark/50">{{ t(key="profile-not-set", lang=lang | default(value='sk')) }}</p>
{% endif %}
</div>
{% endmacro field %}
{% block content %}
<div class="mx-auto max-w-2xl" x-data="{ editing: {% if error %}true{% else %}false{% endif %} }">
<h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="profile-title", lang=lang | default(value='sk')) }}</h1>
<p class="mt-2 text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="profile-intro", lang=lang | default(value='sk')) }}</p>
{% if saved %}
<div class="mt-4 rounded-radius border border-success bg-success/10 px-4 py-3 text-sm text-success" role="status">
{{ t(key="profile-saved", lang=lang | default(value='sk')) }}
</div>
{% endif %}
{% if error %}
{{ ui::alert_danger(message=t(key="profile-company-required", lang=lang | default(value='sk')), extra="mt-4") }}
{% endif %}
<!-- read-only view (default) -->
<div x-show="!editing" class="mt-6 space-y-6">
<fieldset class="space-y-2 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-type", lang=lang | default(value='sk')) }}</legend>
<div class="flex items-center gap-2">
{% if account_type == "company" %}
{{ ui::badge(label=t(key="account-company", lang=lang | default(value='sk')), variant="primary") }}
{% else %}
{{ ui::badge(label=t(key="account-personal", lang=lang | default(value='sk')), variant="neutral") }}
{% endif %}
<span class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-type-locked", lang=lang | default(value='sk')) }}</span>
</div>
</fieldset>
{% if account_type == "company" %}
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company-details", lang=lang | default(value='sk')) }}</legend>
{{ self::field(label=t(key="company-name", lang=lang | default(value='sk')), value=company_name) }}
<div class="grid gap-4 sm:grid-cols-3">
{{ self::field(label=t(key="company-ico", lang=lang | default(value='sk')), value=company_id) }}
{{ self::field(label=t(key="company-dic", lang=lang | default(value='sk')), value=tax_id) }}
{{ self::field(label=t(key="company-icdph", lang=lang | default(value='sk')), value=vat_id) }}
</div>
</fieldset>
{% endif %}
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend>
{{ self::field(label=t(key="checkout-name", lang=lang | default(value='sk')), value=name) }}
{{ self::field(label=t(key="checkout-email", lang=lang | default(value='sk')), value=email) }}
{% if phone %}
{% set phone_full = phone_prefix | default(value='') %}
{% set phone_full = phone_full ~ ' ' ~ phone %}
{{ self::field(label=t(key="checkout-phone", lang=lang | default(value='sk')), value=phone_full) }}
{% else %}
{{ self::field(label=t(key="checkout-phone", lang=lang | default(value='sk')), value='') }}
{% endif %}
</fieldset>
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</legend>
{{ self::field(label=t(key="checkout-address", lang=lang | default(value='sk')), value=address) }}
<div class="grid gap-4 sm:grid-cols-3">
{{ self::field(label=t(key="checkout-city", lang=lang | default(value='sk')), value=city) }}
{{ self::field(label=t(key="checkout-zip", lang=lang | default(value='sk')), value=zip) }}
{{ self::field(label=t(key="checkout-country", lang=lang | default(value='sk')), value=country) }}
</div>
</fieldset>
{{ ui::button(label=t(key="profile-edit", lang=lang | default(value='sk')), type="button", size="px-6 py-2.5 text-sm", attrs='@click="editing = true"') }}
</div>
<!-- edit form -->
<form x-show="editing" x-cloak method="post" action="/account/profile" hx-boost="false" class="mt-6 space-y-6">
<!-- account type is fixed at registration and shown read-only -->
<fieldset class="space-y-2 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-type", lang=lang | default(value='sk')) }}</legend>
<div class="flex items-center gap-2">
{% if account_type == "company" %}
{{ ui::badge(label=t(key="account-company", lang=lang | default(value='sk')), variant="primary") }}
{% else %}
{{ ui::badge(label=t(key="account-personal", lang=lang | default(value='sk')), variant="neutral") }}
{% endif %}
<span class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-type-locked", lang=lang | default(value='sk')) }}</span>
</div>
</fieldset>
{% if account_type == "company" %}
<!-- company billing details (company accounts only) -->
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company-details", lang=lang | default(value='sk')) }}</legend>
<div class="space-y-1.5">
<label for="company_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-name", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
{{ ui::input(name="company_name", id="company_name", value=company_name | default(value=''), autocomplete="organization") }}
</div>
<div class="grid gap-4 sm:grid-cols-3">
<div class="space-y-1.5">
<label for="company_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-ico", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
{{ ui::input(name="company_id", id="company_id", value=company_id | default(value='')) }}
</div>
<div class="space-y-1.5">
<label for="tax_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-dic", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
{{ ui::input(name="tax_id", id="tax_id", value=tax_id | default(value='')) }}
</div>
<div class="space-y-1.5">
<label for="vat_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-icdph", lang=lang | default(value='sk')) }} <span class="text-on-surface/50 dark:text-on-surface-dark/50">({{ t(key="field-optional", lang=lang | default(value='sk')) }})</span></label>
{{ ui::input(name="vat_id", id="vat_id", value=vat_id | default(value='')) }}
</div>
</div>
</fieldset>
{% endif %}
<!-- contact (name/email are managed by the login) -->
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend>
<div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-1.5">
<label for="first_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="profile-first-name", lang=lang | default(value='sk')) }}</label>
{{ ui::input(name="first_name", id="first_name", value=first_name | default(value=''), autocomplete="given-name") }}
</div>
<div class="space-y-1.5">
<label for="last_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="profile-last-name", lang=lang | default(value='sk')) }}</label>
{{ ui::input(name="last_name", id="last_name", value=last_name | default(value=''), autocomplete="family-name") }}
</div>
</div>
<div class="space-y-1.5">
<label class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-email", lang=lang | default(value='sk')) }}</label>
<p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">{{ email }}</p>
</div>
<div class="space-y-1.5">
<label for="phone" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-phone", lang=lang | default(value='sk')) }}</label>
<div class="flex gap-2">
<!-- editable combobox: type freely or pick from the dropdown -->
<div class="relative w-28 shrink-0" @click.outside="prefixOpen = false"
x-data="{ prefixOpen: false, prefix: '{{ phone_prefix | default(value='+421') }}', opts: [
{ v: '+421', l: '🇸🇰 +421' }, { v: '+420', l: '🇨🇿 +420' },
{ v: '+43', l: '🇦🇹 +43' }, { v: '+49', l: '🇩🇪 +49' },
{ v: '+48', l: '🇵🇱 +48' }, { v: '+36', l: '🇭🇺 +36' },
{ v: '+44', l: '🇬🇧 +44' }, { v: '+39', l: '🇮🇹 +39' }, { v: '+33', l: '🇫🇷 +33' }
], get filtered() { return this.opts.filter(o => !this.prefix || o.v.includes(this.prefix)) } }">
<input name="phone_prefix" type="text" x-model="prefix" @focus="prefixOpen = true" @input="prefixOpen = true"
aria-label="{{ t(key='checkout-phone', lang=lang | default(value='sk')) }}" autocomplete="tel-country-code" inputmode="tel"
class="w-full rounded-radius border border-outline bg-surface py-2 pl-3 pr-7 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
<button type="button" tabindex="-1" @click="prefixOpen = !prefixOpen"
class="absolute inset-y-0 right-0 flex w-7 items-center justify-center text-on-surface/60 dark:text-on-surface-dark/60">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
class="size-4 transition-transform" :class="prefixOpen && 'rotate-180'">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</button>
<ul x-show="prefixOpen" x-cloak x-transition
class="absolute z-20 mt-1 max-h-56 w-full overflow-auto rounded-radius border border-outline bg-surface p-1 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt">
<template x-for="o in filtered" :key="o.v">
<li><button type="button" @click="prefix = o.v; prefixOpen = false" x-text="o.l"
class="block w-full rounded-radius px-3 py-1.5 text-left text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark"></button></li>
</template>
</ul>
</div>
{{ ui::input(name="phone", id="phone", type="tel", value=phone | default(value=''), autocomplete="tel", placeholder="900 000 000", attrs='inputmode="tel"') }}
</div>
</div>
</fieldset>
<!-- default shipping address -->
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</legend>
<div class="space-y-1.5">
<label for="address" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-address", lang=lang | default(value='sk')) }}</label>
{{ ui::input(name="address", id="address", value=address | default(value=''), autocomplete="street-address") }}
</div>
<div class="grid gap-4 sm:grid-cols-3">
<div class="space-y-1.5">
<label for="city" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-city", lang=lang | default(value='sk')) }}</label>
{{ ui::input(name="city", id="city", value=city | default(value=''), autocomplete="address-level2") }}
</div>
<div class="space-y-1.5">
<label for="zip" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-zip", lang=lang | default(value='sk')) }}</label>
{{ ui::input(name="zip", id="zip", value=zip | default(value=''), autocomplete="postal-code") }}
</div>
<div class="space-y-1.5">
<label for="country" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-country", lang=lang | default(value='sk')) }}</label>
<div class="relative" @click.outside="countryOpen = false"
x-data="{ countryOpen: false, country: '{{ country | default(value='') }}', opts: [
{ v: '{{ t(key='country-sk', lang=lang | default(value='sk')) }}', l: '🇸🇰 {{ t(key='country-sk', lang=lang | default(value='sk')) }}' },
{ v: '{{ t(key='country-cz', lang=lang | default(value='sk')) }}', l: '🇨🇿 {{ t(key='country-cz', lang=lang | default(value='sk')) }}' },
{ v: '{{ t(key='country-at', lang=lang | default(value='sk')) }}', l: '🇦🇹 {{ t(key='country-at', lang=lang | default(value='sk')) }}' },
{ v: '{{ t(key='country-de', lang=lang | default(value='sk')) }}', l: '🇩🇪 {{ t(key='country-de', lang=lang | default(value='sk')) }}' },
{ v: '{{ t(key='country-pl', lang=lang | default(value='sk')) }}', l: '🇵🇱 {{ t(key='country-pl', lang=lang | default(value='sk')) }}' },
{ v: '{{ t(key='country-hu', lang=lang | default(value='sk')) }}', l: '🇭🇺 {{ t(key='country-hu', lang=lang | default(value='sk')) }}' }
], get filtered() { return this.opts.filter(o => !this.country || o.v.toLowerCase().includes(this.country.toLowerCase())) } }">
<input id="country" name="country" type="text" x-model="country" @focus="countryOpen = true" @input="countryOpen = true"
class="w-full rounded-radius border border-outline bg-surface py-2 pl-3 pr-8 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
<button type="button" tabindex="-1" @click="countryOpen = !countryOpen"
class="absolute inset-y-0 right-0 flex w-8 items-center justify-center text-on-surface/60 dark:text-on-surface-dark/60">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
class="size-4 transition-transform" :class="countryOpen && 'rotate-180'">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</button>
<ul x-show="countryOpen" x-cloak x-transition
class="absolute z-20 mt-1 max-h-56 w-full overflow-auto rounded-radius border border-outline bg-surface p-1 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt">
<template x-for="o in filtered" :key="o.v">
<li><button type="button" @click="country = o.v; countryOpen = false" x-text="o.l"
class="block w-full rounded-radius px-3 py-1.5 text-left text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark"></button></li>
</template>
</ul>
</div>
</div>
</div>
</fieldset>
<div class="flex items-center gap-3">
{{ ui::button(label=t(key="profile-save", lang=lang | default(value='sk')), type="submit", size="px-6 py-2.5 text-sm") }}
{{ ui::button(label=t(key="profile-cancel", lang=lang | default(value='sk')), type="button", variant="outline-secondary", size="px-6 py-2.5 text-sm", attrs='@click="editing = false"') }}
</div>
</form>
</div>
{% endblock content %}

View File

@@ -1,3 +1,4 @@
{% import "macros/ui.html" as ui %}
<!doctype html> <!doctype html>
<html lang="{{ lang | default(value='sk') }}" data-theme="dark"> <html lang="{{ lang | default(value='sk') }}" data-theme="dark">
<head> <head>
@@ -58,7 +59,7 @@
class="fixed inset-y-0 left-0 z-40 flex w-60 flex-col border-r border-outline bg-surface-alt transition-transform duration-300 md:translate-x-0 dark:border-outline-dark dark:bg-surface-dark-alt"> class="fixed inset-y-0 left-0 z-40 flex w-60 flex-col border-r border-outline bg-surface-alt transition-transform duration-300 md:translate-x-0 dark:border-outline-dark dark:bg-surface-dark-alt">
{# Sidebar nav links — adapted from the vendored Penguin UI component {# Sidebar nav links — adapted from the vendored Penguin UI component
assets/views/penguinui/sidebar/simple-sidebar.html: Penguin's link penguinui-components/sidebar/simple-sidebar.html: Penguin's link
treatment (hover:bg-primary/5, focus-visible:underline) with the active treatment (hover:bg-primary/5, focus-visible:underline) with the active
state (bg-primary/10 + text-on-surface-strong) mapped onto our state (bg-primary/10 + text-on-surface-strong) mapped onto our
data-nav / aria-current so markActiveNav() keeps driving it. #} data-nav / aria-current so markActiveNav() keeps driving it. #}
@@ -94,7 +95,7 @@
<a href="/" class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-info underline-offset-2 transition hover:bg-info/5 focus:outline-hidden focus-visible:underline"> <a href="/" class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-info underline-offset-2 transition hover:bg-info/5 focus:outline-hidden focus-visible:underline">
{{ t(key="admin-exit", lang=lang | default(value='sk')) }} {{ t(key="admin-exit", lang=lang | default(value='sk')) }}
</a> </a>
<form method="post" action="/admin/logout"> <form method="post" action="/logout">
<button type="submit" class="flex w-full items-center gap-2 rounded-radius px-2 py-1.5 text-left text-sm font-medium text-danger underline-offset-2 transition hover:bg-danger/5 focus:outline-hidden focus-visible:underline"> <button type="submit" class="flex w-full items-center gap-2 rounded-radius px-2 py-1.5 text-left text-sm font-medium text-danger underline-offset-2 transition hover:bg-danger/5 focus:outline-hidden focus-visible:underline">
{{ t(key="logout", lang=lang | default(value='sk')) }} {{ t(key="logout", lang=lang | default(value='sk')) }}
</button> </button>
@@ -105,13 +106,11 @@
<!-- content column --> <!-- content column -->
<div class="flex min-h-screen flex-col md:ml-60"> <div class="flex min-h-screen flex-col md:ml-60">
<header class="sticky top-0 z-20 flex h-16 items-center gap-4 border-b border-outline bg-surface/95 px-4 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95"> <header class="sticky top-0 z-20 flex h-16 items-center gap-4 border-b border-outline bg-surface/95 px-4 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95">
<button type="button" @click="showSidebar = !showSidebar" :aria-expanded="showSidebar" <!-- Penguin animated hamburger (bars ↔ X) in our ghost-square shell -->
aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}" <button type="button" @click="showSidebar = !showSidebar" :aria-expanded="showSidebar" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
class="inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt md:hidden dark:text-on-surface-dark dark:hover:bg-surface-dark-alt"> class="inline-flex size-9 shrink-0 items-center justify-center rounded-radius bg-transparent text-secondary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 md:hidden dark:text-secondary-dark dark:focus-visible:outline-secondary-dark">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" {{ ui::icon(name="hamburger", size="size-6", attrs='x-show="!showSidebar"') }}
stroke="currentColor" class="size-6"> {{ ui::icon(name="close", size="size-6", attrs='x-cloak x-show="showSidebar"') }}
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button> </button>
<span class="text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong"> <span class="text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">

View File

@@ -10,23 +10,23 @@
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-categories", lang=lang | default(value='sk')) }}</h1> <h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-categories", lang=lang | default(value='sk')) }}</h1>
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-categories-desc", lang=lang | default(value='sk')) }}</p> <p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-categories-desc", lang=lang | default(value='sk')) }}</p>
</div> </div>
{{ ui::button_primary(label=t(key="new-category", lang=lang | default(value='sk')), href="/admin/catalog/categories/new") }} {{ ui::button(label=t(key="new-category", lang=lang | default(value='sk')), href="/admin/catalog/categories/new") }}
</div> </div>
<div class="mt-6 overflow-hidden rounded-radius border border-outline dark:border-outline-dark"> <div class="mt-6 {{ ui::table_wrap_cls() }}">
{% if categories | length > 0 %} {% if categories | length > 0 %}
<table class="w-full text-left text-sm"> <table class="{{ ui::table_cls() }}">
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70"> <thead class="{{ ui::thead_cls() }}">
<tr> <tr>
<th class="px-4 py-3 font-semibold">{{ t(key="name", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="name", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 font-semibold">{{ t(key="admin-products", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="admin-products", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 font-semibold">{{ t(key="status", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="status", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 text-right font-semibold">{{ t(key="actions", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="actions", lang=lang | default(value='sk')), align="text-right") }}
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-outline dark:divide-outline-dark"> <tbody class="{{ ui::tbody_cls() }}">
{% for row in categories %} {% for row in categories %}
<tr class="hover:bg-surface-alt dark:hover:bg-surface-dark-alt"> <tr class="{{ ui::row_cls() }}">
<td class="px-4 py-3 font-medium text-on-surface-strong dark:text-on-surface-dark-strong"> <td class="px-4 py-3 font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
<span style="padding-left: {{ row.depth * 20 }}px" class="inline-flex items-center gap-1.5"> <span style="padding-left: {{ row.depth * 20 }}px" class="inline-flex items-center gap-1.5">
{% if row.depth > 0 %}<span class="text-on-surface/40 dark:text-on-surface-dark/40"></span>{% endif %} {% if row.depth > 0 %}<span class="text-on-surface/40 dark:text-on-surface-dark/40"></span>{% endif %}
@@ -43,11 +43,10 @@
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex flex-wrap justify-end gap-2"> <div class="flex flex-wrap justify-end gap-2">
<a href="/admin/catalog/categories/{{ row.category.id }}/edit" {{ ui::button(variant="outline-secondary", label=t(key="edit", lang=lang | default(value='sk')), href="/admin/catalog/categories/" ~ row.category.id ~ "/edit", size="px-3 py-1.5 text-xs") }}
class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="edit", lang=lang | default(value='sk')) }}</a>
<form method="post" action="/admin/catalog/categories/{{ row.category.id }}/delete" <form method="post" action="/admin/catalog/categories/{{ row.category.id }}/delete"
onsubmit="return confirm('{{ t(key="confirm-delete", lang=lang | default(value='sk')) }}')"> onsubmit="return confirm('{{ t(key="confirm-delete", lang=lang | default(value='sk')) }}')">
<button type="submit" class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-danger transition hover:bg-danger/10 dark:border-outline-dark">{{ t(key="delete", lang=lang | default(value='sk')) }}</button> {{ ui::button(variant="outline-danger", label=t(key="delete", lang=lang | default(value='sk')), type="submit", size="px-3 py-1.5 text-xs") }}
</form> </form>
</div> </div>
</td> </td>
@@ -58,7 +57,7 @@
{% else %} {% else %}
<div class="flex flex-col items-center gap-3 px-4 py-16 text-center"> <div class="flex flex-col items-center gap-3 px-4 py-16 text-center">
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-no-categories", lang=lang | default(value='sk')) }}</p> <p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-no-categories", lang=lang | default(value='sk')) }}</p>
{{ ui::button_primary(label=t(key="new-category", lang=lang | default(value='sk')), href="/admin/catalog/categories/new") }} {{ ui::button(label=t(key="new-category", lang=lang | default(value='sk')), href="/admin/catalog/categories/new") }}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -9,37 +9,40 @@
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong"> <h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
{% if category %}{{ t(key="edit-category", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-category", lang=lang | default(value='sk')) }}{% endif %} {% if category %}{{ t(key="edit-category", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-category", lang=lang | default(value='sk')) }}{% endif %}
</h1> </h1>
{{ ui::button_outline(label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/categories") }} {{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/categories", size="px-3 py-2 text-sm") }}
</div> </div>
<form method="post" enctype="multipart/form-data" <form method="post" enctype="multipart/form-data"
action="{% if category %}/admin/catalog/categories/{{ category.id }}{% else %}/admin/catalog/categories{% endif %}" action="{% if category %}/admin/catalog/categories/{{ category.id }}{% else %}/admin/catalog/categories{% endif %}"
class="mt-6 space-y-5 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt"> class="mt-6 space-y-5 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
{% if category %}
{% set v_name = category.name %}{% set v_slug = category.slug %}{% set v_pos = category.position %}{% set v_desc = category.description | default(value="") %}{% set v_pub = category.published %}
{% else %}
{% set v_name = "" %}{% set v_slug = "" %}{% set v_pos = 0 %}{% set v_desc = "" %}{% set v_pub = false %}
{% endif %}
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="name", lang=lang | default(value='sk')) }}</label> <label for="name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="name", lang=lang | default(value='sk')) }}</label>
<input id="name" name="name" type="text" required value="{% if category %}{{ category.name }}{% endif %}" {{ ui::input(name="name", id="name", required=true, value=v_name) }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="grid gap-5 sm:grid-cols-2"> <div class="grid gap-5 sm:grid-cols-2">
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="slug" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="slug", lang=lang | default(value='sk')) }}</label> <label for="slug" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="slug", lang=lang | default(value='sk')) }}</label>
<input id="slug" name="slug" type="text" value="{% if category %}{{ category.slug }}{% endif %}" {{ ui::input(name="slug", id="slug", value=v_slug, placeholder=t(key='slug-auto', lang=lang | default(value='sk'))) }}
placeholder="{{ t(key='slug-auto', lang=lang | default(value='sk')) }}"
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="position" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="position", lang=lang | default(value='sk')) }}</label> <label for="position" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="position", lang=lang | default(value='sk')) }}</label>
<input id="position" name="position" type="number" value="{% if category %}{{ category.position }}{% else %}0{% endif %}" {{ ui::input(name="position", id="position", type="number", value=v_pos) }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="parent_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="parent-category", lang=lang | default(value='sk')) }}</label> <label for="parent_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="parent-category", lang=lang | default(value='sk')) }}</label>
<div class="relative">
<select id="parent_id" name="parent_id" <select id="parent_id" name="parent_id"
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark"> class="w-full appearance-none rounded-radius border border-outline bg-surface-alt px-4 py-2 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark">
<option value="">{{ t(key="no-parent", lang=lang | default(value='sk')) }}</option> <option value="">{{ t(key="no-parent", lang=lang | default(value='sk')) }}</option>
{% for parent in parents %} {% for parent in parents %}
<option value="{{ parent.id }}" {% if category and category.parent_id == parent.id %}selected{% endif %}> <option value="{{ parent.id }}" {% if category and category.parent_id == parent.id %}selected{% endif %}>
@@ -47,12 +50,13 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="pointer-events-none absolute right-3 top-1/2 size-5 -translate-y-1/2 text-on-surface/60 dark:text-on-surface-dark/60"><path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</div>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="description" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="description", lang=lang | default(value='sk')) }}</label> <label for="description" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="description", lang=lang | default(value='sk')) }}</label>
<textarea id="description" name="description" rows="4" {{ ui::textarea(name="description", id="description", rows="4", value=v_desc) }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">{% if category and category.description %}{{ category.description }}{% endif %}</textarea>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
@@ -60,19 +64,14 @@
{% if category and category.image_id %} {% if category and category.image_id %}
<img src="/images/{{ category.image_id }}" alt="" class="size-24 rounded-radius object-cover"> <img src="/images/{{ category.image_id }}" alt="" class="size-24 rounded-radius object-cover">
{% endif %} {% endif %}
<input id="image" name="image" type="file" accept="image/*" {{ ui::file_input(name="image", id="image", accept="image/*") }}
class="block w-full text-sm text-on-surface file:mr-3 file:rounded-radius file:border-0 file:bg-primary file:px-3 file:py-2 file:text-sm file:font-medium file:text-on-primary dark:text-on-surface-dark dark:file:bg-primary-dark dark:file:text-on-primary-dark">
</div> </div>
<label class="flex items-center gap-2"> {{ ui::checkbox(name="published", id="published", label=t(key="published", lang=lang | default(value='sk')), checked=v_pub) }}
<input type="checkbox" name="published" value="on" {% if category and category.published %}checked{% endif %}
class="size-4 rounded border-outline text-primary focus:ring-primary dark:border-outline-dark">
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="published", lang=lang | default(value='sk')) }}</span>
</label>
<div class="flex gap-3 pt-2"> <div class="flex gap-3 pt-2">
{{ ui::button_primary(label=t(key="save", lang=lang | default(value='sk')), type="submit") }} {{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit") }}
{{ ui::button_outline(label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/categories") }} {{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/categories") }}
</div> </div>
</form> </form>
{% endblock content %} {% endblock content %}

View File

@@ -9,68 +9,68 @@
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong"> <h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
{% if product %}{{ t(key="edit-product", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-product", lang=lang | default(value='sk')) }}{% endif %} {% if product %}{{ t(key="edit-product", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-product", lang=lang | default(value='sk')) }}{% endif %}
</h1> </h1>
{{ ui::button_outline(label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/products") }} {{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/products", size="px-3 py-2 text-sm") }}
</div> </div>
<form method="post" enctype="multipart/form-data" <form method="post" enctype="multipart/form-data"
action="{% if product %}/admin/catalog/products/{{ product.id }}{% else %}/admin/catalog/products{% endif %}" action="{% if product %}/admin/catalog/products/{{ product.id }}{% else %}/admin/catalog/products{% endif %}"
class="mt-6 space-y-5 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt"> class="mt-6 space-y-5 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
{% if product %}
{% set v_name = product.name %}{% set v_price = product.price %}{% set v_currency = product.currency %}{% set v_stock = product.stock %}{% set v_sku = product.sku | default(value="") %}{% set v_slug = product.slug %}{% set v_desc = product.description | default(value="") %}{% set v_pub = product.published %}
{% else %}
{% set v_name = "" %}{% set v_price = "" %}{% set v_currency = "EUR" %}{% set v_stock = 0 %}{% set v_sku = "" %}{% set v_slug = "" %}{% set v_desc = "" %}{% set v_pub = false %}
{% endif %}
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="name", lang=lang | default(value='sk')) }}</label> <label for="name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="name", lang=lang | default(value='sk')) }}</label>
<input id="name" name="name" type="text" required value="{% if product %}{{ product.name }}{% endif %}" {{ ui::input(name="name", id="name", required=true, value=v_name) }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="grid gap-5 sm:grid-cols-2"> <div class="grid gap-5 sm:grid-cols-2">
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="price" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="price", lang=lang | default(value='sk')) }}</label> <label for="price" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="price", lang=lang | default(value='sk')) }}</label>
<input id="price" name="price" type="text" inputmode="decimal" required value="{% if product %}{{ product.price }}{% endif %}" {{ ui::input(name="price", id="price", required=true, value=v_price, placeholder="0.00", attrs='inputmode="decimal"') }}
placeholder="0.00"
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="currency" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="currency", lang=lang | default(value='sk')) }}</label> <label for="currency" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="currency", lang=lang | default(value='sk')) }}</label>
<input id="currency" name="currency" type="text" maxlength="3" value="{% if product %}{{ product.currency }}{% else %}EUR{% endif %}" {{ ui::input(name="currency", id="currency", value=v_currency, attrs='maxlength="3"', extra="uppercase") }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm uppercase text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
</div> </div>
<div class="grid gap-5 sm:grid-cols-2"> <div class="grid gap-5 sm:grid-cols-2">
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="stock" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="stock", lang=lang | default(value='sk')) }}</label> <label for="stock" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="stock", lang=lang | default(value='sk')) }}</label>
<input id="stock" name="stock" type="number" min="0" value="{% if product %}{{ product.stock }}{% else %}0{% endif %}" {{ ui::input(name="stock", id="stock", type="number", value=v_stock, attrs='min="0"') }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="sku" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="sku", lang=lang | default(value='sk')) }}</label> <label for="sku" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="sku", lang=lang | default(value='sk')) }}</label>
<input id="sku" name="sku" type="text" value="{% if product and product.sku %}{{ product.sku }}{% endif %}" {{ ui::input(name="sku", id="sku", value=v_sku) }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="category_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="category", lang=lang | default(value='sk')) }}</label> <label for="category_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="category", lang=lang | default(value='sk')) }}</label>
<div class="relative">
<select id="category_id" name="category_id" <select id="category_id" name="category_id"
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark"> class="w-full appearance-none rounded-radius border border-outline bg-surface-alt px-4 py-2 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark">
<option value="">{{ t(key="no-category", lang=lang | default(value='sk')) }}</option> <option value="">{{ t(key="no-category", lang=lang | default(value='sk')) }}</option>
{% for category in categories %} {% for category in categories %}
<option value="{{ category.id }}" {% if product and product.category_id == category.id %}selected{% endif %}>{{ category.name }}</option> <option value="{{ category.id }}" {% if product and product.category_id == category.id %}selected{% endif %}>{{ category.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="pointer-events-none absolute right-3 top-1/2 size-5 -translate-y-1/2 text-on-surface/60 dark:text-on-surface-dark/60"><path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</div>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="slug" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="slug", lang=lang | default(value='sk')) }}</label> <label for="slug" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="slug", lang=lang | default(value='sk')) }}</label>
<input id="slug" name="slug" type="text" value="{% if product %}{{ product.slug }}{% endif %}" {{ ui::input(name="slug", id="slug", value=v_slug, placeholder=t(key='slug-auto', lang=lang | default(value='sk'))) }}
placeholder="{{ t(key='slug-auto', lang=lang | default(value='sk')) }}"
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="description" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="description", lang=lang | default(value='sk')) }}</label> <label for="description" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="description", lang=lang | default(value='sk')) }}</label>
<textarea id="description" name="description" rows="5" {{ ui::textarea(name="description", id="description", rows="5", value=v_desc) }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">{% if product and product.description %}{{ product.description }}{% endif %}</textarea>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
@@ -78,19 +78,14 @@
{% if product and product.image %} {% if product and product.image %}
<img src="/images/{{ product.image }}" alt="" class="size-24 rounded-radius object-cover"> <img src="/images/{{ product.image }}" alt="" class="size-24 rounded-radius object-cover">
{% endif %} {% endif %}
<input id="image" name="image" type="file" accept="image/*" {{ ui::file_input(name="image", id="image", accept="image/*") }}
class="block w-full text-sm text-on-surface file:mr-3 file:rounded-radius file:border-0 file:bg-primary file:px-3 file:py-2 file:text-sm file:font-medium file:text-on-primary dark:text-on-surface-dark dark:file:bg-primary-dark dark:file:text-on-primary-dark">
</div> </div>
<label class="flex items-center gap-2"> {{ ui::checkbox(name="published", id="published", label=t(key="published", lang=lang | default(value='sk')), checked=v_pub) }}
<input type="checkbox" name="published" value="on" {% if product and product.published %}checked{% endif %}
class="size-4 rounded border-outline text-primary focus:ring-primary dark:border-outline-dark">
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="published", lang=lang | default(value='sk')) }}</span>
</label>
<div class="flex gap-3 pt-2"> <div class="flex gap-3 pt-2">
{{ ui::button_primary(label=t(key="save", lang=lang | default(value='sk')), type="submit") }} {{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit") }}
{{ ui::button_outline(label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/products") }} {{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/products") }}
</div> </div>
</form> </form>
{% endblock content %} {% endblock content %}

View File

@@ -10,24 +10,24 @@
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-products", lang=lang | default(value='sk')) }}</h1> <h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-products", lang=lang | default(value='sk')) }}</h1>
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-products-desc", lang=lang | default(value='sk')) }}</p> <p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-products-desc", lang=lang | default(value='sk')) }}</p>
</div> </div>
{{ ui::button_primary(label=t(key="new-product", lang=lang | default(value='sk')), href="/admin/catalog/products/new") }} {{ ui::button(label=t(key="new-product", lang=lang | default(value='sk')), href="/admin/catalog/products/new") }}
</div> </div>
<div class="mt-6 overflow-hidden rounded-radius border border-outline dark:border-outline-dark"> <div class="mt-6 {{ ui::table_wrap_cls() }}">
{% if products | length > 0 %} {% if products | length > 0 %}
<table class="w-full text-left text-sm"> <table class="{{ ui::table_cls() }}">
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70"> <thead class="{{ ui::thead_cls() }}">
<tr> <tr>
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 font-semibold">{{ t(key="price", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="price", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 font-semibold">{{ t(key="stock", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="stock", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 font-semibold">{{ t(key="status", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="status", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 text-right font-semibold">{{ t(key="actions", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="actions", lang=lang | default(value='sk')), align="text-right") }}
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-outline dark:divide-outline-dark"> <tbody class="{{ ui::tbody_cls() }}">
{% for product in products %} {% for product in products %}
<tr class="hover:bg-surface-alt dark:hover:bg-surface-dark-alt"> <tr class="{{ ui::row_cls() }}">
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
{% if product.image %} {% if product.image %}
@@ -52,13 +52,11 @@
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex flex-wrap justify-end gap-2"> <div class="flex flex-wrap justify-end gap-2">
<a href="/admin/catalog/products/{{ product.id }}/edit" {{ ui::button(variant="outline-secondary", label=t(key="edit", lang=lang | default(value='sk')), href="/admin/catalog/products/" ~ product.id ~ "/edit", size="px-3 py-1.5 text-xs") }}
class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="edit", lang=lang | default(value='sk')) }}</a> {{ ui::button(variant="outline-secondary", label=t(key="view", lang=lang | default(value='sk')), href="/shop/" ~ product.slug, size="px-3 py-1.5 text-xs") }}
<a href="/shop/{{ product.slug }}"
class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="view", lang=lang | default(value='sk')) }}</a>
<form method="post" action="/admin/catalog/products/{{ product.id }}/delete" <form method="post" action="/admin/catalog/products/{{ product.id }}/delete"
onsubmit="return confirm('{{ t(key="confirm-delete", lang=lang | default(value='sk')) }}')"> onsubmit="return confirm('{{ t(key="confirm-delete", lang=lang | default(value='sk')) }}')">
<button type="submit" class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-danger transition hover:bg-danger/10 dark:border-outline-dark">{{ t(key="delete", lang=lang | default(value='sk')) }}</button> {{ ui::button(variant="outline-danger", label=t(key="delete", lang=lang | default(value='sk')), type="submit", size="px-3 py-1.5 text-xs") }}
</form> </form>
</div> </div>
</td> </td>
@@ -69,7 +67,7 @@
{% else %} {% else %}
<div class="flex flex-col items-center gap-3 px-4 py-16 text-center"> <div class="flex flex-col items-center gap-3 px-4 py-16 text-center">
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-no-products", lang=lang | default(value='sk')) }}</p> <p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-no-products", lang=lang | default(value='sk')) }}</p>
{{ ui::button_primary(label=t(key="new-product", lang=lang | default(value='sk')), href="/admin/catalog/products/new") }} {{ ui::button(label=t(key="new-product", lang=lang | default(value='sk')), href="/admin/catalog/products/new") }}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,53 +0,0 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="login-title", lang=lang | default(value='sk')) }}{% endblock title %}
{% block content %}
<div class="mx-auto mt-8 max-w-sm">
<div
class="rounded-radius border border-outline bg-surface-alt shadow-sm dark:border-outline-dark dark:bg-surface-dark-alt">
<div
class="flex items-center justify-between border-b border-outline px-5 py-3 dark:border-outline-dark">
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="nav-admin", lang=lang | default(value='sk')) }}
</span>
{{ ui::badge(label=t(key="auth", lang=lang | default(value='sk')), variant="danger") }}
</div>
<div class="p-5">
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="login-auth", lang=lang | default(value='sk')) }}
</h1>
{% if error %}
{{ ui::alert_danger(message=t(key="login-error", lang=lang | default(value='sk')), extra="mt-3") }}
{% endif %}
<form method="post" action="/admin/login" hx-boost="false" class="mt-4 flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="email"
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="login-email", lang=lang | default(value='sk')) }}
</label>
<input type="email" id="email" name="email" required autofocus
autocomplete="email"
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark dark:focus:ring-primary-dark">
</div>
<div class="flex flex-col gap-1">
<label for="password"
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="login-password", lang=lang | default(value='sk')) }}
</label>
<input type="password" id="password" name="password" required
autocomplete="current-password"
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark dark:focus:ring-primary-dark">
</div>
{{ ui::button_primary(label=t(key="login-auth", lang=lang | default(value='sk')), type="submit", extra="mt-1 w-full") }}
</form>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -7,21 +7,21 @@
{% block content %} {% block content %}
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-orders", lang=lang | default(value='sk')) }}</h1> <h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-orders", lang=lang | default(value='sk')) }}</h1>
<div class="mt-6 overflow-hidden rounded-radius border border-outline dark:border-outline-dark"> <div class="mt-6 {{ ui::table_wrap_cls() }}">
{% if orders | length > 0 %} {% if orders | length > 0 %}
<table class="w-full text-left text-sm"> <table class="{{ ui::table_cls() }}">
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70"> <thead class="{{ ui::thead_cls() }}">
<tr> <tr>
<th class="px-4 py-3 font-semibold">{{ t(key="order-number", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="order-number", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 font-semibold">{{ t(key="order-customer", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="order-customer", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 font-semibold">{{ t(key="order-status", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="order-status", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 text-right font-semibold">{{ t(key="order-total", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="order-total", lang=lang | default(value='sk')), align="text-right") }}
<th class="px-4 py-3"></th> {{ ui::th(label="") }}
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-outline dark:divide-outline-dark"> <tbody class="{{ ui::tbody_cls() }}">
{% for order in orders %} {% for order in orders %}
<tr class="hover:bg-surface-alt dark:hover:bg-surface-dark-alt"> <tr class="{{ ui::row_cls() }}">
<td class="px-4 py-3 font-mono font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.order_number }}</td> <td class="px-4 py-3 font-mono font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.order_number }}</td>
<td class="px-4 py-3">{{ order.email }}</td> <td class="px-4 py-3">{{ order.email }}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
@@ -29,7 +29,7 @@
</td> </td>
<td class="px-4 py-3 text-right tabular-nums">{{ order.total }} {{ order.currency }}</td> <td class="px-4 py-3 text-right tabular-nums">{{ order.total }} {{ order.currency }}</td>
<td class="px-4 py-3 text-right"> <td class="px-4 py-3 text-right">
<a href="/admin/orders/{{ order.id }}" class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="view", lang=lang | default(value='sk')) }}</a> {{ ui::button(variant="outline-secondary", label=t(key="view", lang=lang | default(value='sk')), href="/admin/orders/" ~ order.id, size="px-3 py-1.5 text-xs") }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -7,7 +7,7 @@
{% block content %} {% block content %}
<div class="flex flex-wrap items-center justify-between gap-3"> <div class="flex flex-wrap items-center justify-between gap-3">
<h1 class="font-mono text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.order_number }}</h1> <h1 class="font-mono text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.order_number }}</h1>
{{ ui::button_outline(label=t(key="admin-orders", lang=lang | default(value='sk')), href="/admin/orders") }} {{ ui::button(variant="outline-secondary", label=t(key="admin-orders", lang=lang | default(value='sk')), href="/admin/orders", size="px-3 py-2 text-sm") }}
</div> </div>
{% if ship_error %} {% if ship_error %}
@@ -16,16 +16,16 @@
<div class="mt-6 grid gap-6 lg:grid-cols-3"> <div class="mt-6 grid gap-6 lg:grid-cols-3">
<div class="space-y-6 lg:col-span-2"> <div class="space-y-6 lg:col-span-2">
<div class="overflow-hidden rounded-radius border border-outline dark:border-outline-dark"> <div class="{{ ui::table_wrap_cls() }}">
<table class="w-full text-left text-sm"> <table class="{{ ui::table_cls() }}">
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70"> <thead class="{{ ui::thead_cls() }}">
<tr> <tr>
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 font-semibold">{{ t(key="quantity", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="quantity", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 text-right font-semibold">{{ t(key="order-total", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="order-total", lang=lang | default(value='sk')), align="text-right") }}
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-outline dark:divide-outline-dark"> <tbody class="{{ ui::tbody_cls() }}">
{% for item in items %} {% for item in items %}
<tr> <tr>
<td class="px-4 py-3">{{ item.product_name }}</td> <td class="px-4 py-3">{{ item.product_name }}</td>
@@ -34,7 +34,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot class="border-t border-outline dark:border-outline-dark"> <tfoot class="{{ ui::tfoot_cls() }}">
<tr> <tr>
<td colspan="2" class="px-4 py-3 text-right font-semibold">{{ t(key="order-total", lang=lang | default(value='sk')) }}</td> <td colspan="2" class="px-4 py-3 text-right font-semibold">{{ t(key="order-total", lang=lang | default(value='sk')) }}</td>
<td class="px-4 py-3 text-right font-bold tabular-nums text-primary dark:text-primary-dark">{{ order.total }} {{ order.currency }}</td> <td class="px-4 py-3 text-right font-bold tabular-nums text-primary dark:text-primary-dark">{{ order.total }} {{ order.currency }}</td>
@@ -52,6 +52,15 @@
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.email }}</p> <p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.email }}</p>
{% if order.phone %}<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.phone }}</p>{% endif %} {% if order.phone %}<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.phone }}</p>{% endif %}
</div> </div>
{% if order.account_type == "company" %}
<div>
<p class="text-xs uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-company-details", lang=lang | default(value='sk')) }}</p>
<p class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.company_name }}</p>
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ t(key="company-ico", lang=lang | default(value='sk')) }}: {{ order.company_id }}</p>
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ t(key="company-dic", lang=lang | default(value='sk')) }}: {{ order.tax_id }}</p>
{% if order.vat_id %}<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ t(key="company-icdph", lang=lang | default(value='sk')) }}: {{ order.vat_id }}</p>{% endif %}
</div>
{% endif %}
<div> <div>
<p class="text-xs uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</p> <p class="text-xs uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</p>
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.address }}<br>{{ order.zip }} {{ order.city }}<br>{{ order.country }}</p> <p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.address }}<br>{{ order.zip }} {{ order.city }}<br>{{ order.country }}</p>
@@ -85,8 +94,7 @@
{{ t(key="order-tracking", lang=lang | default(value='sk')) }}: <span class="font-mono font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.tracking_number }}</span> {{ t(key="order-tracking", lang=lang | default(value='sk')) }}: <span class="font-mono font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.tracking_number }}</span>
</p> </p>
{% if order.label_url %} {% if order.label_url %}
<a href="{{ order.label_url }}" target="_blank" rel="noopener" {{ ui::button(variant="outline-secondary", label=t(key="order-label", lang=lang | default(value='sk')), href=order.label_url, size="px-3 py-1.5 text-xs", attrs='target="_blank" rel="noopener"') }}
class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="order-label", lang=lang | default(value='sk')) }}</a>
{% endif %} {% endif %}
{% elif carrier == "none" %} {% elif carrier == "none" %}
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-manual-fulfillment", lang=lang | default(value='sk')) }}</p> <p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-manual-fulfillment", lang=lang | default(value='sk')) }}</p>
@@ -96,20 +104,23 @@
onsubmit="return confirm('{{ t(key="order-send-confirm", lang=lang | default(value='sk')) }}')"> onsubmit="return confirm('{{ t(key="order-send-confirm", lang=lang | default(value='sk')) }}')">
{% set carrier_up = carrier | upper %} {% set carrier_up = carrier | upper %}
{% set ship_label = t(key="order-send-to-carrier", lang=lang | default(value='sk')) ~ " " ~ carrier_up %} {% set ship_label = t(key="order-send-to-carrier", lang=lang | default(value='sk')) ~ " " ~ carrier_up %}
{{ ui::button_primary(label=ship_label, type="submit", extra="w-full") }} {{ ui::button(label=ship_label, type="submit", extra="w-full") }}
</form> </form>
{% endif %} {% endif %}
</div> </div>
<form method="post" action="/admin/orders/{{ order.id }}/status" class="space-y-3 rounded-radius border border-outline bg-surface p-5 dark:border-outline-dark dark:bg-surface-dark-alt"> <form method="post" action="/admin/orders/{{ order.id }}/status" class="space-y-3 rounded-radius border border-outline bg-surface p-5 dark:border-outline-dark dark:bg-surface-dark-alt">
<label for="status" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="order-status", lang=lang | default(value='sk')) }}</label> <label for="status" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="order-status", lang=lang | default(value='sk')) }}</label>
<div class="relative">
<select id="status" name="status" <select id="status" name="status"
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark"> class="w-full appearance-none rounded-radius border border-outline bg-surface-alt px-4 py-2 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark">
{% for status in statuses %} {% for status in statuses %}
<option value="{{ status }}" {% if order.status == status %}selected{% endif %}>{{ t(key="order-status-" ~ status, lang=lang | default(value='sk')) }}</option> <option value="{{ status }}" {% if order.status == status %}selected{% endif %}>{{ t(key="order-status-" ~ status, lang=lang | default(value='sk')) }}</option>
{% endfor %} {% endfor %}
</select> </select>
{{ ui::button_primary(label=t(key="order-update-status", lang=lang | default(value='sk')), type="submit", extra="w-full") }} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="pointer-events-none absolute right-3 top-1/2 size-5 -translate-y-1/2 text-on-surface/60 dark:text-on-surface-dark/60"><path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</div>
{{ ui::button(label=t(key="order-update-status", lang=lang | default(value='sk')), type="submit", extra="w-full") }}
</form> </form>
</aside> </aside>
</div> </div>

View File

@@ -20,15 +20,10 @@
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="price-{{ method.id }}" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="price", lang=lang | default(value='sk')) }}</label> <label for="price-{{ method.id }}" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="price", lang=lang | default(value='sk')) }}</label>
<input id="price-{{ method.id }}" name="price" type="text" inputmode="decimal" value="{{ method.price }}" {{ ui::input(name="price", id="price-" ~ method.id, value=method.price, width="w-28", attrs='inputmode="decimal"') }}
class="w-28 rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<label class="flex items-center gap-2 pb-2"> <div class="pb-2">{{ ui::checkbox(name="enabled", label=t(key="shipping-enabled", lang=lang | default(value='sk')), checked=method.enabled) }}</div>
<input type="checkbox" name="enabled" value="on" {% if method.enabled %}checked{% endif %} {{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit", extra="ml-auto") }}
class="size-4 rounded border-outline text-primary focus:ring-primary dark:border-outline-dark">
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="shipping-enabled", lang=lang | default(value='sk')) }}</span>
</label>
{{ ui::button_primary(label=t(key="save", lang=lang | default(value='sk')), type="submit", extra="ml-auto") }}
</form> </form>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -0,0 +1,67 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="login-title", lang=lang | default(value='sk')) }}{% endblock title %}
{% block content %}
<div class="mx-auto mt-8 max-w-sm">
<div
class="rounded-radius border border-outline bg-surface-alt shadow-sm dark:border-outline-dark dark:bg-surface-dark-alt">
<div
class="flex items-center justify-between border-b border-outline px-5 py-3 dark:border-outline-dark">
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="brand", lang=lang | default(value='sk')) }}
</span>
{{ ui::badge(label=t(key="auth", lang=lang | default(value='sk')), variant="primary") }}
</div>
<div class="p-5">
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="login-title", lang=lang | default(value='sk')) }}
</h1>
{% if error == "unverified" %}
{{ ui::alert_danger(message=t(key="login-error-unverified", lang=lang | default(value='sk')), extra="mt-3") }}
<p class="mt-2 text-sm text-on-surface dark:text-on-surface-dark">
<a href="/resend-verification" class="font-medium text-primary underline-offset-2 hover:underline dark:text-primary-dark">{{ t(key="login-resend", lang=lang | default(value='sk')) }}</a>
</p>
{% elif error %}
{{ ui::alert_danger(message=t(key="login-error", lang=lang | default(value='sk')), extra="mt-3") }}
{% endif %}
<form method="post" action="/login" hx-boost="false" class="mt-4 flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="email"
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="login-email", lang=lang | default(value='sk')) }}
</label>
{{ ui::input(name="email", id="email", type="email", required=true, autocomplete="email", attrs="autofocus") }}
</div>
<div class="flex flex-col gap-1">
<label for="password"
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="login-password", lang=lang | default(value='sk')) }}
</label>
{{ ui::input(name="password", id="password", type="password", required=true, autocomplete="current-password") }}
</div>
{{ ui::button(label=t(key="login-auth", lang=lang | default(value='sk')), type="submit", extra="mt-1 w-full") }}
</form>
<div class="mt-5 flex items-center gap-3 text-xs text-on-surface/50 dark:text-on-surface-dark/50">
<span class="h-px flex-1 bg-outline dark:bg-outline-dark"></span>
{{ t(key="auth-or", lang=lang | default(value='sk')) }}
<span class="h-px flex-1 bg-outline dark:bg-outline-dark"></span>
</div>
{{ ui::button(label=t(key="auth-google", lang=lang | default(value='sk')), href="/api/oauth2/google", variant="outline-secondary", attrs='hx-boost="false"', extra="mt-4 w-full", icon='<svg class="size-4" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="#FFC107" d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z"/><path fill="#FF3D00" d="m6.306 14.691 6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z"/><path fill="#4CAF50" d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.91 11.91 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z"/><path fill="#1976D2" d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002 6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z"/></svg>') }}
<p class="mt-4 text-sm text-on-surface dark:text-on-surface-dark">
{{ t(key="login-no-account", lang=lang | default(value='sk')) }}
<a href="/register"
class="font-medium text-primary underline-offset-2 hover:underline dark:text-primary-dark">{{ t(key="nav-register", lang=lang | default(value='sk')) }}</a>
</p>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,96 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="register-title", lang=lang | default(value='sk')) }}{% endblock title %}
{% block content %}
<div class="mx-auto mt-8 max-w-sm">
<div
class="rounded-radius border border-outline bg-surface-alt shadow-sm dark:border-outline-dark dark:bg-surface-dark-alt">
<div
class="flex items-center justify-between border-b border-outline px-5 py-3 dark:border-outline-dark">
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="brand", lang=lang | default(value='sk')) }}
</span>
{{ ui::badge(label=t(key="auth", lang=lang | default(value='sk')), variant="primary") }}
</div>
<div class="p-5">
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="register-title", lang=lang | default(value='sk')) }}
</h1>
{% if error == "exists" %}
{{ ui::alert_danger(message=t(key="register-error-exists", lang=lang | default(value='sk')), extra="mt-3") }}
{% elif error == "mismatch" %}
{{ ui::alert_danger(message=t(key="set-password-mismatch", lang=lang | default(value='sk')), extra="mt-3") }}
{% elif error == "weak" %}
{{ ui::alert_danger(message=t(key="set-password-weak", lang=lang | default(value='sk')), extra="mt-3") }}
{% elif error %}
{{ ui::alert_danger(message=t(key="register-error-invalid", lang=lang | default(value='sk')), extra="mt-3") }}
{% endif %}
<form method="post" action="/register" hx-boost="false" class="mt-4 flex flex-col gap-4"
x-data="{ password: '', confirm: '' }">
<div class="flex flex-col gap-1.5">
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-type", lang=lang | default(value='sk')) }}</span>
<div class="grid grid-cols-2 gap-2">
<label class="flex cursor-pointer items-center gap-2 rounded-radius border border-outline px-3 py-2 text-sm transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
{{ ui::radio(name="account_type", value="personal", checked=true) }}
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-personal", lang=lang | default(value='sk')) }}</span>
</label>
<label class="flex cursor-pointer items-center gap-2 rounded-radius border border-outline px-3 py-2 text-sm transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
{{ ui::radio(name="account_type", value="company") }}
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company", lang=lang | default(value='sk')) }}</span>
</label>
</div>
<span class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-type-locked", lang=lang | default(value='sk')) }}</span>
</div>
<div class="flex flex-col gap-1">
<label for="email"
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="login-email", lang=lang | default(value='sk')) }}
</label>
{{ ui::input(name="email", id="email", type="email", required=true, autocomplete="email", attrs="autofocus") }}
</div>
<div class="flex flex-col gap-1">
<label for="password"
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="login-password", lang=lang | default(value='sk')) }}
</label>
{{ ui::input(name="password", id="password", type="password", required=true, autocomplete="new-password", attrs='x-model="password"') }}
</div>
<div class="flex flex-col gap-1">
<label for="password_confirm"
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="set-password-confirm", lang=lang | default(value='sk')) }}
</label>
{{ ui::input(name="password_confirm", id="password_confirm", type="password", required=true, autocomplete="new-password", attrs='x-model="confirm"') }}
<span x-cloak x-show="confirm.length > 0 && password !== confirm"
class="text-xs text-danger dark:text-danger">
{{ t(key="set-password-mismatch", lang=lang | default(value='sk')) }}
</span>
</div>
{{ ui::button(label=t(key="register-submit", lang=lang | default(value='sk')), type="submit", extra="mt-1 w-full", attrs=':disabled="password !== confirm"') }}
</form>
<div class="mt-5 flex items-center gap-3 text-xs text-on-surface/50 dark:text-on-surface-dark/50">
<span class="h-px flex-1 bg-outline dark:bg-outline-dark"></span>
{{ t(key="auth-or", lang=lang | default(value='sk')) }}
<span class="h-px flex-1 bg-outline dark:bg-outline-dark"></span>
</div>
{{ ui::button(label=t(key="auth-google", lang=lang | default(value='sk')), href="/api/oauth2/google", variant="outline-secondary", attrs='hx-boost="false"', extra="mt-4 w-full", icon='<svg class="size-4" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="#FFC107" d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z"/><path fill="#FF3D00" d="m6.306 14.691 6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z"/><path fill="#4CAF50" d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.91 11.91 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z"/><path fill="#1976D2" d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002 6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z"/></svg>') }}
<p class="mt-4 text-sm text-on-surface dark:text-on-surface-dark">
{{ t(key="login-have-account", lang=lang | default(value='sk')) }}
<a href="/login"
class="font-medium text-primary underline-offset-2 hover:underline dark:text-primary-dark">{{ t(key="nav-login", lang=lang | default(value='sk')) }}</a>
</p>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="resend-verification-title", lang=lang | default(value='sk')) }}{% endblock title %}
{% block content %}
<div class="mx-auto mt-8 max-w-sm">
<div class="rounded-radius border border-outline bg-surface-alt shadow-sm dark:border-outline-dark dark:bg-surface-dark-alt">
<div class="flex items-center justify-between border-b border-outline px-5 py-3 dark:border-outline-dark">
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="brand", lang=lang | default(value='sk')) }}</span>
{{ ui::badge(label=t(key="auth", lang=lang | default(value='sk')), variant="primary") }}
</div>
<div class="p-5">
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="resend-verification-title", lang=lang | default(value='sk')) }}</h1>
{% if done %}
<div class="mt-3 rounded-radius border border-success bg-success/10 px-4 py-3 text-sm text-success" role="status">
{{ t(key="resend-verification-done", lang=lang | default(value='sk')) }}
</div>
<p class="mt-4 text-sm text-on-surface dark:text-on-surface-dark">
<a href="/login" class="font-medium text-primary underline-offset-2 hover:underline dark:text-primary-dark">{{ t(key="nav-login", lang=lang | default(value='sk')) }}</a>
</p>
{% else %}
<p class="mt-1 text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="resend-verification-intro", lang=lang | default(value='sk')) }}</p>
<form method="post" action="/resend-verification" hx-boost="false" class="mt-4 flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="email" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="login-email", lang=lang | default(value='sk')) }}</label>
{{ ui::input(name="email", id="email", type="email", required=true, autocomplete="email", attrs="autofocus") }}
</div>
{{ ui::button(label=t(key="resend-verification-submit", lang=lang | default(value='sk')), type="submit", extra="mt-1 w-full") }}
</form>
{% endif %}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="set-password-title", lang=lang | default(value='sk')) }}{% endblock title %}
{% block content %}
<div class="mx-auto mt-8 max-w-sm">
<div class="rounded-radius border border-outline bg-surface-alt shadow-sm dark:border-outline-dark dark:bg-surface-dark-alt">
<div class="flex items-center justify-between border-b border-outline px-5 py-3 dark:border-outline-dark">
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="brand", lang=lang | default(value='sk')) }}</span>
{{ ui::badge(label=t(key="auth", lang=lang | default(value='sk')), variant="primary") }}
</div>
<div class="p-5">
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="set-password-title", lang=lang | default(value='sk')) }}</h1>
{% if not valid %}
{{ ui::alert_danger(message=t(key="set-password-invalid", lang=lang | default(value='sk')), extra="mt-3") }}
<p class="mt-4 text-sm text-on-surface dark:text-on-surface-dark">
<a href="/login" class="font-medium text-primary underline-offset-2 hover:underline dark:text-primary-dark">{{ t(key="nav-login", lang=lang | default(value='sk')) }}</a>
</p>
{% else %}
<p class="mt-1 text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="set-password-intro", lang=lang | default(value='sk')) }}</p>
{% if error == "mismatch" %}
{{ ui::alert_danger(message=t(key="set-password-mismatch", lang=lang | default(value='sk')), extra="mt-3") }}
{% elif error == "weak" %}
{{ ui::alert_danger(message=t(key="set-password-weak", lang=lang | default(value='sk')), extra="mt-3") }}
{% endif %}
<form method="post" action="/set-password" hx-boost="false" class="mt-4 flex flex-col gap-4">
<input type="hidden" name="token" value="{{ token }}">
<div class="flex flex-col gap-1">
<label for="password" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="set-password-new", lang=lang | default(value='sk')) }}</label>
{{ ui::input(name="password", id="password", type="password", required=true, autocomplete="new-password", attrs="autofocus") }}
</div>
<div class="flex flex-col gap-1">
<label for="password_confirm" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="set-password-confirm", lang=lang | default(value='sk')) }}</label>
{{ ui::input(name="password_confirm", id="password_confirm", type="password", required=true, autocomplete="new-password") }}
</div>
{{ ui::button(label=t(key="set-password-submit", lang=lang | default(value='sk')), type="submit", extra="mt-1 w-full") }}
</form>
{% endif %}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,27 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{% if ok %}{{ t(key="verify-ok-title", lang=lang | default(value='sk')) }}{% else %}{{ t(key="verify-fail-title", lang=lang | default(value='sk')) }}{% endif %}{% endblock title %}
{% block content %}
<div class="mx-auto mt-8 max-w-sm">
<div
class="rounded-radius border border-outline bg-surface-alt p-5 shadow-sm dark:border-outline-dark dark:bg-surface-dark-alt">
{% if ok %}
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="verify-ok-title", lang=lang | default(value='sk')) }}
</h1>
<p class="mt-3 text-sm text-on-surface dark:text-on-surface-dark">
{{ t(key="verify-ok-body", lang=lang | default(value='sk')) }}
</p>
{{ ui::button(label=t(key="login-auth", lang=lang | default(value='sk')), href="/login", extra="mt-4 w-full") }}
{% else %}
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="verify-fail-title", lang=lang | default(value='sk')) }}
</h1>
{{ ui::alert_danger(message=t(key="verify-fail-body", lang=lang | default(value='sk')), extra="mt-3") }}
{{ ui::button(label=t(key="nav-login", lang=lang | default(value='sk')), href="/login", variant="outline-primary", extra="mt-4 w-full") }}
{% endif %}
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="verify-sent-title", lang=lang | default(value='sk')) }}{% endblock title %}
{% block content %}
<div class="mx-auto mt-8 max-w-sm">
<div
class="rounded-radius border border-outline bg-surface-alt p-5 shadow-sm dark:border-outline-dark dark:bg-surface-dark-alt">
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="verify-sent-title", lang=lang | default(value='sk')) }}
</h1>
<p class="mt-3 text-sm text-on-surface dark:text-on-surface-dark">
{{ t(key="verify-sent-body", lang=lang | default(value='sk')) }}
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ email }}</span>
</p>
{{ ui::button(label=t(key="nav-login", lang=lang | default(value='sk')), href="/login", variant="outline-primary", extra="mt-4 w-full") }}
</div>
</div>
{% endblock content %}

View File

@@ -1,3 +1,4 @@
{% import "macros/ui.html" as ui %}
<!doctype html> <!doctype html>
<html lang="{{ lang | default(value='sk') }}" data-theme="dark"> <html lang="{{ lang | default(value='sk') }}" data-theme="dark">
<head> <head>
@@ -66,31 +67,34 @@
class="sticky top-0 z-30 border-b border-outline bg-surface/95 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95"> class="sticky top-0 z-30 border-b border-outline bg-surface/95 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95">
<nav x-data="{ mobile: false }" class="mx-auto flex max-w-7xl items-center gap-4 px-4 py-3"> <nav x-data="{ mobile: false }" class="mx-auto flex max-w-7xl items-center gap-4 px-4 py-3">
<!-- category sidebar toggle (mobile only) --> <!-- category sidebar toggle (mobile only) -->
<button type="button" @click="cats = !cats" :aria-expanded="cats" {% set hamburger_icon = ui::icon(name="hamburger", size="size-6") %}
aria-label="{{ t(key='categories', lang=lang | default(value='sk')) }}" {{ ui::icon_button(aria_label=t(key='categories', lang=lang | default(value='sk')), attrs='@click="cats = !cats" :aria-expanded="cats"', extra="lg:hidden", icon=hamburger_icon) }}
class="inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt lg:hidden dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button>
<a href="/" <a href="/"
class="text-lg font-bold tracking-tight text-on-surface-strong dark:text-on-surface-dark-strong"> class="text-lg font-bold tracking-tight text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="brand", lang=lang | default(value='sk')) }} {{ t(key="brand", lang=lang | default(value='sk')) }}
</a> </a>
<!-- desktop links --> <!-- desktop links — Penguin navbar link treatment via ui::nav_link -->
<ul class="ml-2 hidden items-center gap-1 md:flex"> <ul class="ml-2 hidden items-center gap-6 md:flex">
<li><a href="/" data-nav="/" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li> <li>{{ ui::nav_link(label=t(key="nav-home", lang=lang | default(value='sk')), href="/", data_nav="/") }}</li>
<li><a href="/shop" data-nav="/shop" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li> <li>{{ ui::nav_link(label=t(key="nav-shop", lang=lang | default(value='sk')), href="/shop", data_nav="/shop") }}</li>
{% if logged_in_admin %} {% if logged_in_admin %}
<li><a href="/admin/dashboard" hx-boost="false" data-nav="/admin" class="rounded-radius px-3 py-1.5 text-sm font-medium text-warning transition hover:bg-surface-alt dark:hover:bg-surface-dark-alt">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li> <li>{{ ui::nav_link(label=t(key="admin-title", lang=lang | default(value='sk')), href="/admin/dashboard", data_nav="/admin", variant="warning", attrs='hx-boost="false"') }}</li>
<li> <li>
<form method="post" action="/admin/logout" hx-boost="false"> <form method="post" action="/logout" hx-boost="false">
<button type="submit" class="rounded-radius px-3 py-1.5 text-sm font-medium text-danger transition hover:bg-surface-alt dark:hover:bg-surface-dark-alt">{{ t(key="logout", lang=lang | default(value='sk')) }}</button> <button type="submit" class="text-sm font-medium text-danger underline-offset-2 transition hover:opacity-75 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form>
</li>
{% elif logged_in_customer %}
<li>{{ ui::nav_link(label=t(key="nav-profile", lang=lang | default(value='sk')), href="/account/profile", data_nav="/account") }}</li>
<li>
<form method="post" action="/logout" hx-boost="false">
<button type="submit" class="text-sm font-medium text-danger underline-offset-2 transition hover:opacity-75 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form> </form>
</li> </li>
{% else %} {% else %}
<li><a href="/admin/login" data-nav="/admin/login" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li> <li>{{ ui::nav_link(label=t(key="nav-login", lang=lang | default(value='sk')), href="/login", data_nav="/login") }}</li>
<li>{{ ui::nav_link(label=t(key="nav-register", lang=lang | default(value='sk')), href="/register", data_nav="/register") }}</li>
{% endif %} {% endif %}
</ul> </ul>
@@ -102,10 +106,8 @@
x-init="count = cartCount(); ['htmx:afterSwap', 'htmx:afterRequest'].forEach(function (e) { window.addEventListener(e, function () { count = cartCount() }) })" x-init="count = cartCount(); ['htmx:afterSwap', 'htmx:afterRequest'].forEach(function (e) { window.addEventListener(e, function () { count = cartCount() }) })"
aria-label="{{ t(key='cart-title', lang=lang | default(value='sk')) }}" aria-label="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
title="{{ t(key='cart-title', lang=lang | default(value='sk')) }}" title="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
class="relative inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark-alt"> class="relative inline-flex size-9 shrink-0 items-center justify-center rounded-radius bg-transparent text-secondary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 dark:text-secondary-dark dark:focus-visible:outline-secondary-dark">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5"> {{ ui::icon(name="cart") }}
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
</svg>
<span x-show="count > 0" x-cloak x-text="count" <span x-show="count > 0" x-cloak x-text="count"
class="absolute -right-1 -top-1 inline-flex min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-4 text-on-primary dark:bg-primary-dark dark:text-on-primary-dark"></span> class="absolute -right-1 -top-1 inline-flex min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-4 text-on-primary dark:bg-primary-dark dark:text-on-primary-dark"></span>
</a> </a>
@@ -114,31 +116,38 @@
{% include "partials/settings_dropdown.html" %} {% include "partials/settings_dropdown.html" %}
</div> </div>
<!-- mobile hamburger --> <!-- mobile hamburger — Penguin animated icon swap (bars ↔ X), kept in
<button type="button" @click="mobile = !mobile" :aria-expanded="mobile" our ghost-square icon-button shell for consistency with cart/gear -->
aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}" <button type="button" @click="mobile = !mobile" :aria-expanded="mobile" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
class="inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt md:hidden dark:text-on-surface-dark dark:hover:bg-surface-dark-alt"> class="inline-flex size-9 shrink-0 items-center justify-center rounded-radius bg-transparent text-secondary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 md:hidden dark:text-secondary-dark dark:focus-visible:outline-secondary-dark">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" {{ ui::icon(name="hamburger", size="size-6", attrs='x-show="!mobile"') }}
stroke="currentColor" class="size-6"> {{ ui::icon(name="close", size="size-6", attrs='x-cloak x-show="mobile"') }}
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button> </button>
</div> </div>
<!-- mobile menu panel --> <!-- mobile menu panel — Penguin sidebar-style menu rows (hover:bg-primary/5,
underline focus), active state via data-nav + markActiveNav() -->
<ul x-show="mobile" x-cloak @click.outside="mobile = false" x-transition <ul x-show="mobile" x-cloak @click.outside="mobile = false" x-transition
class="absolute inset-x-0 top-full mx-4 mt-2 flex flex-col gap-1 rounded-radius border border-outline bg-surface p-2 shadow-lg md:hidden dark:border-outline-dark dark:bg-surface-dark-alt"> class="absolute inset-x-0 top-full mx-4 mt-2 flex flex-col gap-1 rounded-radius border border-outline bg-surface p-2 shadow-lg md:hidden dark:border-outline-dark dark:bg-surface-dark-alt">
<li><a href="/" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li> <li><a href="/" data-nav="/" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/shop" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li> <li><a href="/shop" data-nav="/shop" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li>
{% if logged_in_admin %} {% if logged_in_admin %}
<li><a href="/admin/dashboard" hx-boost="false" class="block rounded-radius px-3 py-2 text-sm font-medium text-warning hover:bg-surface-alt dark:hover:bg-surface-dark">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li> <li><a href="/admin/dashboard" hx-boost="false" data-nav="/admin" class="block rounded-radius px-3 py-2 text-sm font-medium text-warning underline-offset-2 transition hover:bg-primary/5 focus:outline-hidden focus-visible:underline">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li>
<li> <li>
<form method="post" action="/admin/logout" hx-boost="false"> <form method="post" action="/logout" hx-boost="false">
<button type="submit" class="block w-full rounded-radius px-3 py-2 text-left text-sm font-medium text-danger hover:bg-surface-alt dark:hover:bg-surface-dark">{{ t(key="logout", lang=lang | default(value='sk')) }}</button> <button type="submit" class="block w-full rounded-radius px-3 py-2 text-left text-sm font-medium text-danger underline-offset-2 transition hover:bg-primary/5 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form>
</li>
{% elif logged_in_customer %}
<li><a href="/account/profile" data-nav="/account" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-profile", lang=lang | default(value='sk')) }}</a></li>
<li>
<form method="post" action="/logout" hx-boost="false">
<button type="submit" class="block w-full rounded-radius px-3 py-2 text-left text-sm font-medium text-danger underline-offset-2 transition hover:bg-primary/5 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form> </form>
</li> </li>
{% else %} {% else %}
<li><a href="/admin/login" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li> <li><a href="/login" data-nav="/login" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-login", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/register" data-nav="/register" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-register", lang=lang | default(value='sk')) }}</a></li>
{% endif %} {% endif %}
</ul> </ul>
</nav> </nav>
@@ -165,7 +174,7 @@
<!-- toast notifications: fire from anywhere with toast('message'). <!-- toast notifications: fire from anywhere with toast('message').
Adapted from the vendored Penguin UI component Adapted from the vendored Penguin UI component
(assets/views/penguinui/toast-notification/stacking-toast-notification.html): (penguinui-components/toast-notification/stacking-toast-notification.html):
the docs-only demo trigger buttons are omitted and the malformed quotes on the docs-only demo trigger buttons are omitted and the malformed quotes on
the upstream dismiss-button <svg> tags are fixed. --> the upstream dismiss-button <svg> tags are fixed. -->
<div x-data="{ <div x-data="{

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="brand", lang=lang | default(value='sk')) }}{% endblock title %} {% block title %}{{ t(key="brand", lang=lang | default(value='sk')) }}{% endblock title %}

View File

@@ -1,45 +1,92 @@
{# Reusable UI macros adapted from vendored Penguin UI components. {# Reusable UI macros adapted from vendored Penguin UI components.
These are OUR adaptation layer; the byte-for-byte upstream sources live under These are OUR adaptation layer; the byte-for-byte upstream sources live under
assets/views/penguinui/. Tailwind sees the full literal class strings here penguinui-components/. Tailwind sees the full literal class strings here
(assets/css/app.css has @source "../views"), so every branch must spell its (assets/css/app.css has @source "../views"), so every branch must spell its
classes out in full — never build class names by concatenation. classes out in full — never build class names by concatenation.
Usage: Usage:
{% import "macros/ui.html" as ui %} {% import "macros/ui.html" as ui %}
{{ ui::button_primary(label=t(key="save", lang=lang)) }} {{ ui::button(label=t(key="save", lang=lang)) }} {# default primary #}
{{ ui::button_primary(label="Add", attrs='hx-post="/x"' | safe) }} {{ ui::button(label="Add", attrs='hx-post="/x"' | safe) }}
{{ ui::button_outline(label="Cancel", href="/back") }} {{ ui::button(label="Cancel", variant="outline-secondary", href="/back") }}
{{ ui::button(label="Send", size="px-6 py-2.5 text-sm") }} {# keep a non-default size #}
{{ ui::badge(label="Published", variant="success") }} {{ ui::badge(label="Published", variant="success") }}
Notes: Notes:
- Macros can't see template context vars (e.g. `lang`); pass already-translated - Macros can't see template context vars (e.g. `lang`); pass already-translated
strings as `label`. strings as `label`.
- `attrs` is injected raw (caller must pass it through `| safe`); use it for - `attrs` is injected raw (caller must pass it through `| safe`); use it for
htmx / name / value / @click etc. For buttons whose attrs carry nested htmx / name / value / @click / :disabled etc. For buttons whose attrs carry
quotes (e.g. hx-on with toast(...)), keep them inline instead. nested quotes (e.g. hx-on with toast(...)), keep them inline instead.
- Source: penguinui/buttons/{default,outline,ghost}-button.html and - `pad` is the size (default Penguin "px-4 py-2"); override to preserve an
penguinui/badge/soft-color-badge.html. Upstream's color-button typos existing size rather than normalizing it.
(text-onDanger etc.) are fixed to our real tokens (text-on-danger). #} - The button class strings are the **verbatim** Penguin variants from
penguinui/buttons/{default,outline,ghost}-button.html (only `inline-flex
items-center justify-center` is added so <a> and w-full render correctly,
and the upstream `text-onDanger`/`text-onSuccess`… token typos are fixed to
our real `text-on-*` tokens). `variant` selects a Penguin variant:
solid : primary (default) | secondary | danger | success | warning | info
outline : outline-primary | outline-secondary | outline-alternate | outline-danger
ghost : ghost-primary | ghost-secondary | ghost-danger #}
{% macro button_primary(label, type="button", href="", attrs="", extra="") -%} {% macro button(label, variant="primary", type="button", href="", attrs="", extra="", icon="", size="px-4 py-2 text-sm") -%}
{% if href %}<a href="{{ href }}"{% else %}<button type="{{ type }}"{% endif %} class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-radius border border-primary bg-primary px-4 py-2 text-center text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark {{ extra }}" {{ attrs | safe }}>{{ label }}</{% if href %}a{% else %}button{% endif %}> {%- if variant == "secondary" -%}{% set cls = "border border-secondary bg-secondary text-on-secondary focus-visible:outline-secondary dark:border-secondary-dark dark:bg-secondary-dark dark:text-on-secondary-dark dark:focus-visible:outline-secondary-dark" -%}
{%- endmacro button_primary %} {%- elif variant == "danger" -%}{% set cls = "border border-danger bg-danger text-on-danger focus-visible:outline-danger dark:bg-danger dark:border-danger dark:text-on-danger dark:focus-visible:outline-danger" -%}
{%- elif variant == "success" -%}{% set cls = "border border-success bg-success text-on-success focus-visible:outline-success dark:bg-success dark:border-success dark:text-on-success dark:focus-visible:outline-success" -%}
{%- elif variant == "warning" -%}{% set cls = "border border-warning bg-warning text-on-warning focus-visible:outline-warning dark:bg-warning dark:border-warning dark:text-on-warning dark:focus-visible:outline-warning" -%}
{%- elif variant == "info" -%}{% set cls = "border border-info bg-info text-on-info focus-visible:outline-info dark:bg-info dark:border-info dark:text-on-info dark:focus-visible:outline-info" -%}
{%- elif variant == "outline-primary" -%}{% set cls = "border border-primary bg-transparent text-primary focus-visible:outline-primary dark:border-primary-dark dark:text-primary-dark dark:focus-visible:outline-primary-dark" -%}
{%- elif variant == "outline-secondary" -%}{% set cls = "border border-secondary bg-transparent text-secondary focus-visible:outline-secondary dark:border-secondary-dark dark:text-secondary-dark dark:focus-visible:outline-secondary-dark" -%}
{%- elif variant == "outline-alternate" -%}{% set cls = "border border-outline bg-transparent text-outline focus-visible:outline-outline dark:border-outline-dark dark:text-outline-dark dark:focus-visible:outline-outline-dark" -%}
{%- elif variant == "outline-danger" -%}{% set cls = "border border-danger bg-transparent text-danger focus-visible:outline-danger dark:border-danger dark:text-danger dark:focus-visible:outline-danger" -%}
{%- elif variant == "ghost-primary" -%}{% set cls = "bg-transparent text-primary focus-visible:outline-primary dark:text-primary-dark dark:focus-visible:outline-primary-dark" -%}
{%- elif variant == "ghost-secondary" -%}{% set cls = "bg-transparent text-secondary focus-visible:outline-secondary dark:text-secondary-dark dark:focus-visible:outline-secondary-dark" -%}
{%- elif variant == "ghost-danger" -%}{% set cls = "bg-transparent text-danger focus-visible:outline-danger dark:text-danger dark:focus-visible:outline-danger" -%}
{%- else -%}{% set cls = "border border-primary bg-primary text-on-primary focus-visible:outline-primary dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark" -%}
{%- endif -%}
{% if href %}<a href="{{ href }}"{% else %}<button type="{{ type }}"{% endif %} class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-radius {{ size }} text-center font-medium tracking-wide transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 {{ cls }} {{ extra }}" {{ attrs | safe }}>{{ icon | safe }}{{ label }}</{% if href %}a{% else %}button{% endif %}>
{%- endmacro button %}
{% macro button_outline(label, type="button", href="", attrs="", extra="") -%} {# Icon-only button (square). Penguin ghost treatment (bg-transparent,
{% if href %}<a href="{{ href }}"{% else %}<button type="{{ type }}"{% endif %} class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-radius border border-outline bg-surface-alt px-4 py-2 text-center text-sm font-medium tracking-wide text-on-surface transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-outline-strong active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark dark:focus-visible:outline-outline-dark-strong {{ extra }}" {{ attrs | safe }}>{{ label }}</{% if href %}a{% else %}button{% endif %}> hover:opacity-75); pass the raw <svg> as `icon`, an accessible name via
{%- endmacro button_outline %} `aria_label`/`sr`, and any Alpine/htmx via `attrs` (raw). variant ∈
ghost-secondary (default) | ghost-primary | ghost-danger | ghost-alternate. #}
{% macro icon_button(icon, variant="ghost-secondary", type="button", href="", attrs="", extra="", aria_label="", sr="", size="size-9") -%}
{%- if variant == "ghost-primary" -%}{% set cls = "text-primary focus-visible:outline-primary dark:text-primary-dark dark:focus-visible:outline-primary-dark" -%}
{%- elif variant == "ghost-danger" -%}{% set cls = "text-danger focus-visible:outline-danger dark:text-danger dark:focus-visible:outline-danger" -%}
{%- elif variant == "ghost-alternate" -%}{% set cls = "text-outline focus-visible:outline-outline dark:text-outline-dark dark:focus-visible:outline-outline-dark" -%}
{%- else -%}{% set cls = "text-secondary focus-visible:outline-secondary dark:text-secondary-dark dark:focus-visible:outline-secondary-dark" -%}
{%- endif -%}
{% if href %}<a href="{{ href }}"{% else %}<button type="{{ type }}"{% endif %}{% if aria_label %} aria-label="{{ aria_label }}" title="{{ aria_label }}"{% endif %} class="inline-flex shrink-0 items-center justify-center rounded-radius bg-transparent {{ size }} transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 {{ cls }} {{ extra }}" {{ attrs | safe }}>{{ icon | safe }}{% if sr %}<span class="sr-only">{{ sr }}</span>{% endif %}</{% if href %}a{% else %}button{% endif %}>
{%- endmacro icon_button %}
{% macro button_danger(label, type="button", href="", attrs="", extra="") -%} {# Inline icon set — the Heroicons-style SVGs that were duplicated across
{% if href %}<a href="{{ href }}"{% else %}<button type="{{ type }}"{% endif %} class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-radius border border-danger bg-danger px-4 py-2 text-center text-sm font-medium tracking-wide text-on-danger transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:focus-visible:outline-danger {{ extra }}" {{ attrs | safe }}>{{ label }}</{% if href %}a{% else %}button{% endif %}> base.html / admin/base.html (hamburger, close, cart). Penguin ships no icon
{%- endmacro button_danger %} library, so this is pure dedup, not a port. `size` sets the box (default
size-5), `extra` adds classes, `attrs` is raw (x-show / x-cloak etc.). Icons
{% macro button_ghost(label, type="button", href="", attrs="", extra="") -%} are decorative: aria-hidden is baked in — put the accessible name on the
{% if href %}<a href="{{ href }}"{% else %}<button type="{{ type }}"{% endif %} class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-radius bg-transparent px-4 py-2 text-center text-sm font-medium tracking-wide text-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:text-primary-dark dark:focus-visible:outline-primary-dark {{ extra }}" {{ attrs | safe }}>{{ label }}</{% if href %}a{% else %}button{% endif %}> enclosing button/link. The chevron dropdown arrows (checkout, _sidebar) stay
{%- endmacro button_ghost %} inline at their call sites because they carry nested-quote Alpine :class
bindings (see the attrs note at the top of this file). name ∈
hamburger (default) | close | cart. #}
{% macro icon(name, size="size-5", extra="", attrs="") -%}
{%- if name == "cart" -%}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="{{ size }}{% if extra %} {{ extra }}{% endif %}" aria-hidden="true" {{ attrs | safe }}><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" /></svg>
{%- elif name == "close" -%}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="{{ size }}{% if extra %} {{ extra }}{% endif %}" aria-hidden="true" {{ attrs | safe }}><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>
{%- else -%}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="{{ size }}{% if extra %} {{ extra }}{% endif %}" aria-hidden="true" {{ attrs | safe }}><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /></svg>
{%- endif -%}
{%- endmacro icon %}
{# Compact danger alert (form/inline errors). Adapted from {# Compact danger alert (form/inline errors). Adapted from
penguinui/alert/default-alert.html (danger variant), trimmed to a single line penguinui/alert/default-alert.html (danger variant), trimmed to a single line
with the danger icon. #} with the danger icon. #}
{# Required-field marker: a red asterisk appended to a field label. #}
{% macro req() -%}
<span class="ml-0.5 text-danger" aria-hidden="true">*</span>
{%- endmacro req %}
{% macro alert_danger(message, extra="") -%} {% macro alert_danger(message, extra="") -%}
<div class="flex w-full items-center gap-2 overflow-hidden rounded-radius border border-danger bg-danger/10 px-3 py-2 text-sm text-danger {{ extra }}" role="alert"> <div class="flex w-full items-center gap-2 overflow-hidden rounded-radius border border-danger bg-danger/10 px-3 py-2 text-sm text-danger {{ extra }}" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 shrink-0" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 shrink-0" aria-hidden="true">
@@ -65,3 +112,104 @@
<span class="inline-flex w-fit overflow-hidden rounded-radius border border-outline bg-surface text-xs font-medium text-on-surface dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark"><span class="bg-surface-alt/40 px-2 py-1 dark:bg-surface-dark-alt/40">{{ label }}</span></span> <span class="inline-flex w-fit overflow-hidden rounded-radius border border-outline bg-surface text-xs font-medium text-on-surface dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark"><span class="bg-surface-alt/40 px-2 py-1 dark:bg-surface-dark-alt/40">{{ label }}</span></span>
{%- endif %} {%- endif %}
{%- endmacro badge %} {%- endmacro badge %}
{# ---- Form controls. Verbatim Penguin classes from
penguinui/{text-input,text-area,select,checkbox,file-input}/default-*.html.
These macros emit only the control (callers keep their own <label>/layout), so
text-color utilities are added here (upstream sets them on the wrapper div). #}
{# Text/email/number/password input. #}
{% macro input(name, type="text", id="", value="", placeholder="", required=false, autocomplete="", attrs="", extra="", width="w-full") -%}
<input {% if id %}id="{{ id }}" {% endif %}name="{{ name }}" type="{{ type }}"{% if value != "" %} value="{{ value }}"{% endif %}{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if required %} required{% endif %}{% if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %} class="{{ width }} rounded-radius border border-outline bg-surface-alt px-2 py-2 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark {{ extra }}" {{ attrs | safe }}/>
{%- endmacro input %}
{% macro textarea(name, id="", value="", rows="3", placeholder="", required=false, attrs="", extra="") -%}
<textarea {% if id %}id="{{ id }}" {% endif %}name="{{ name }}" rows="{{ rows }}"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if required %} required{% endif %} class="w-full rounded-radius border border-outline bg-surface-alt px-2.5 py-2 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark {{ extra }}" {{ attrs | safe }}>{{ value }}</textarea>
{%- endmacro textarea %}
{# File input. #}
{% macro file_input(name, id="", accept="", attrs="", extra="") -%}
<input {% if id %}id="{{ id }}" {% endif %}name="{{ name }}" type="file"{% if accept %} accept="{{ accept }}"{% endif %} class="w-full overflow-clip rounded-radius border border-outline bg-surface-alt/50 text-sm text-on-surface file:mr-4 file:border-none file:bg-surface-alt file:px-4 file:py-2 file:font-medium file:text-on-surface-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:file:bg-surface-dark-alt dark:file:text-on-surface-dark-strong dark:focus-visible:outline-primary-dark {{ extra }}" {{ attrs | safe }}/>
{%- endmacro file_input %}
{# Checkbox (full Penguin control: custom box + check icon + label text). #}
{% macro checkbox(name, label, id="", value="on", checked=false, attrs="", extra="") -%}
<label {% if id %}for="{{ id }}" {% endif %}class="flex items-center gap-2 text-sm font-medium text-on-surface dark:text-on-surface-dark has-checked:text-on-surface-strong dark:has-checked:text-on-surface-dark-strong has-disabled:cursor-not-allowed has-disabled:opacity-75 {{ extra }}">
<span class="relative flex items-center">
<input {% if id %}id="{{ id }}" {% endif %}name="{{ name }}" value="{{ value }}" type="checkbox"{% if checked %} checked{% endif %} class="before:content[''] peer relative size-4 appearance-none overflow-hidden rounded-sm border border-outline bg-surface-alt before:absolute before:inset-0 checked:border-primary checked:before:bg-primary focus:outline-2 focus:outline-offset-2 focus:outline-outline-strong checked:focus:outline-primary active:outline-offset-0 disabled:cursor-not-allowed dark:border-outline-dark dark:bg-surface-dark-alt dark:checked:border-primary-dark dark:checked:before:bg-primary-dark dark:focus:outline-outline-dark-strong dark:checked:focus:outline-primary-dark" {{ attrs | safe }}/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="4" class="pointer-events-none invisible absolute left-1/2 top-1/2 size-3 -translate-x-1/2 -translate-y-1/2 text-on-primary peer-checked:visible dark:text-on-primary-dark">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
</svg>
</span>
<span>{{ label }}</span>
</label>
{%- endmacro checkbox %}
{# Radio dot (verbatim Penguin custom radio from penguinui/radio/radio-with-container.html).
Emits ONLY the <input> (the styled dot) — callers keep their own card-style
<label> wrapper (e.g. checkout's has-[:checked]:border-primary cards). Use
`attrs` for x-model / required etc.; callers whose @change mixes nested
single+double quotes (carrier loop) spell this class out inline instead. #}
{% macro radio(name, value="", id="", checked=false, attrs="", extra="") -%}
<input {% if id %}id="{{ id }}" {% endif %}name="{{ name }}" type="radio" value="{{ value }}"{% if checked %} checked{% endif %} class="before:content[''] relative h-4 w-4 appearance-none rounded-full border border-outline bg-surface before:invisible before:absolute before:left-1/2 before:top-1/2 before:h-1.5 before:w-1.5 before:-translate-x-1/2 before:-translate-y-1/2 before:rounded-full before:bg-on-primary checked:border-primary checked:bg-primary checked:before:visible focus:outline-2 focus:outline-offset-2 focus:outline-outline-strong checked:focus:outline-primary disabled:cursor-not-allowed dark:border-outline-dark dark:bg-surface-dark dark:before:bg-on-primary-dark dark:checked:border-primary-dark dark:checked:bg-primary-dark dark:focus:outline-outline-dark-strong dark:checked:focus:outline-primary-dark {{ extra }}" {{ attrs | safe }}/>
{%- endmacro radio %}
{# ---- Table chrome. The same wrapper/thead/tbody/row/tfoot class strings were
copy-pasted across 5 admin/shop tables; centralized here so the styling has a
single source of truth. Tera has no slot/{% raw %}{% call %}{% endraw %} mechanism, so cells stay
inline (their content varies too much to macro-ize: images, htmx forms, Alpine
inputs, badges) and these macros expose just the shared CLASS STRINGS used as
`class="{{ ui::thead_cls() }}"`. Adopts Penguin default-table.html's
`w-full overflow-x-auto` wrapper so wide tables scroll on mobile.
Skeleton:
<div class="{{ ui::table_wrap_cls() }}">
<table class="{{ ui::table_cls() }}">
<thead class="{{ ui::thead_cls() }}"><tr>{{ ui::th(label="Name") }}{{ ui::th(label="Total", align="text-right") }}</tr></thead>
<tbody class="{{ ui::tbody_cls() }}">
<tr class="{{ ui::row_cls() }}"><td class="px-4 py-3"></td></tr> {# row_cls = hover; omit for non-interactive rows #}
</tbody>
</table>
</div> #}
{% macro table_wrap_cls() -%}
overflow-hidden w-full overflow-x-auto rounded-radius border border-outline dark:border-outline-dark
{%- endmacro table_wrap_cls %}
{% macro table_cls() -%}
w-full text-left text-sm
{%- endmacro table_cls %}
{% macro thead_cls() -%}
border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70
{%- endmacro thead_cls %}
{% macro tbody_cls() -%}
divide-y divide-outline dark:divide-outline-dark
{%- endmacro tbody_cls %}
{% macro row_cls() -%}
hover:bg-surface-alt dark:hover:bg-surface-dark-alt
{%- endmacro row_cls %}
{% macro tfoot_cls() -%}
border-t border-outline dark:border-outline-dark
{%- endmacro tfoot_cls %}
{# Header cell. align ∈ "" (left, default) | "text-right". Pass label="" for the
empty actions column. #}
{% macro th(label, align="") -%}
<th class="px-4 py-3 font-semibold{% if align %} {{ align }}{% endif %}">{{ label }}</th>
{%- endmacro th %}
{# Top-nav link. Penguin navbar/default-navbar.html link treatment: text-only,
underline on focus, hover:text-primary, active (aria-current=page, set by
markActiveNav() via data-nav) = font-semibold + primary. Matches the ported
sidebars. variant ∈ default | warning (admin) | danger (logout-style links).
Logout itself stays an inline <form><button> (not an <a>, so not this macro). #}
{% macro nav_link(label, href, data_nav="", variant="default", attrs="") -%}
{%- if variant == "warning" -%}{% set c = "text-warning hover:opacity-75 dark:text-warning" -%}
{%- elif variant == "danger" -%}{% set c = "text-danger hover:opacity-75 dark:text-danger" -%}
{%- else -%}{% set c = "text-on-surface hover:text-primary aria-[current=page]:font-semibold aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark" -%}
{%- endif -%}
<a href="{{ href }}"{% if data_nav %} data-nav="{{ data_nav }}"{% endif %} class="text-sm font-medium underline-offset-2 transition focus:outline-hidden focus-visible:underline {{ c }}" {{ attrs | safe }}>{{ label }}</a>
{%- endmacro nav_link %}

View File

@@ -2,7 +2,7 @@
to kill the former ~100-line copy-paste duplication. to kill the former ~100-line copy-paste duplication.
Adapted from the vendored Penguin UI component Adapted from the vendored Penguin UI component
assets/views/penguinui/dropdowns/dropdown-with-click.html: Penguin's dropdown penguinui-components/dropdowns/dropdown-with-click.html: Penguin's dropdown
menu container + item treatment. Deviations: kept our gear icon-only trigger menu container + item treatment. Deviations: kept our gear icon-only trigger
and our core-Alpine open / @click.outside toggle (upstream's x-trap / $focus and our core-Alpine open / @click.outside toggle (upstream's x-trap / $focus
need the Alpine Focus plugin, which we don't bundle); item hover uses need the Alpine Focus plugin, which we don't bundle); item hover uses
@@ -11,17 +11,7 @@
The host template provides the wrapper The host template provides the wrapper
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative ..."> <div x-data="{ open: false }" @keydown.escape="open = false" class="relative ...">
so it controls its own positioning (e.g. ml-auto in admin). #} so it controls its own positioning (e.g. ml-auto in admin). #}
<button type="button" @click="open = !open" :aria-expanded="open" {{ ui::icon_button(aria_label=t(key='settings', lang=lang | default(value='sk')), attrs='@click="open = !open" :aria-expanded="open"', icon='<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>') }}
aria-label="{{ t(key='settings', lang=lang | default(value='sk')) }}"
title="{{ t(key='settings', lang=lang | default(value='sk')) }}"
class="inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-outline-strong dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:focus-visible:outline-outline-dark-strong">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</button>
<div x-show="open" x-cloak @click.outside="open = false" x-transition.origin.top.right <div x-show="open" x-cloak @click.outside="open = false" x-transition.origin.top.right
class="absolute right-0 mt-2 flex w-56 flex-col overflow-hidden rounded-radius border border-outline bg-surface-alt py-1 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt" class="absolute right-0 mt-2 flex w-56 flex-col overflow-hidden rounded-radius border border-outline bg-surface-alt py-1 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt"
role="menu"> role="menu">
@@ -43,21 +33,24 @@
<p class="mt-1 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60"> <p class="mt-1 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
{{ t(key="settings-theme", lang=lang | default(value='sk')) }} {{ t(key="settings-theme", lang=lang | default(value='sk')) }}
</p> </p>
<div x-data="{ theme: currentTheme() }" @theme:changed.document="theme = $event.detail"> {# Theme picker: Penguin UI's switch toggle (penguinui-components/toggle/default-toggle.html),
<button type="button" @click="setTheme('system')" role="menuitem" adapted from a standalone checkbox into a light/dark switch wired to our existing
class="flex w-full items-center justify-between px-4 py-2 text-sm text-on-surface transition hover:bg-primary/5 hover:text-on-surface-strong focus-visible:bg-primary/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong"> setTheme()/currentTheme() helpers in base.html (which drive <html data-theme>). The `dark`
<span>{{ t(key="theme-system", lang=lang | default(value='sk')) }}</span> state initialises from the resolved theme — so a stored 'system' preference still reflects
<span x-show="theme === 'system'" class="text-primary dark:text-primary-dark"></span> the current appearance — and stays in sync via the theme:changed event the helpers dispatch.
</button> Toggling writes an explicit light/dark choice (the old tri-state 'system' option is dropped,
<button type="button" @click="setTheme('light')" role="menuitem" matching Penguin's binary toggle). #}
class="flex w-full items-center justify-between px-4 py-2 text-sm text-on-surface transition hover:bg-primary/5 hover:text-on-surface-strong focus-visible:bg-primary/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong"> <div class="px-4 py-2"
<span>{{ t(key="theme-light", lang=lang | default(value='sk')) }}</span> x-data="{
<span x-show="theme === 'light'" class="text-primary dark:text-primary-dark"></span> labels: { light: '{{ t(key='theme-light', lang=lang | default(value='sk')) }}', dark: '{{ t(key='theme-dark', lang=lang | default(value='sk')) }}' },
</button> dark: document.documentElement.getAttribute('data-theme') === 'dark'
<button type="button" @click="setTheme('dark')" role="menuitem" }"
class="flex w-full items-center justify-between px-4 py-2 text-sm text-on-surface transition hover:bg-primary/5 hover:text-on-surface-strong focus-visible:bg-primary/10 focus-visible:text-on-surface-strong focus-visible:outline-hidden dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong"> @theme:changed.document="dark = document.documentElement.getAttribute('data-theme') === 'dark'">
<span>{{ t(key="theme-dark", lang=lang | default(value='sk')) }}</span> <label for="themeToggle" class="inline-flex w-full cursor-pointer items-center justify-between gap-3">
<span x-show="theme === 'dark'" class="text-primary dark:text-primary-dark"></span> <span class="text-sm font-medium text-on-surface dark:text-on-surface-dark" x-text="dark ? labels.dark : labels.light"></span>
</button> <input id="themeToggle" type="checkbox" class="peer sr-only" role="switch"
:checked="dark" @change="setTheme($event.target.checked ? 'dark' : 'light')" />
<div class="relative h-6 w-11 after:h-5 after:w-5 peer-checked:after:translate-x-5 rounded-full border border-outline bg-surface-alt after:absolute after:bottom-0 after:left-[0.0625rem] after:top-0 after:my-auto after:rounded-full after:bg-on-surface after:transition-all after:content-[''] peer-checked:bg-primary peer-checked:after:bg-on-primary peer-focus:outline-2 peer-focus:outline-offset-2 peer-focus:outline-outline-strong peer-focus:peer-checked:outline-primary peer-active:outline-offset-0 dark:border-outline-dark dark:bg-surface-dark-alt dark:after:bg-on-surface-dark dark:peer-checked:bg-primary-dark dark:peer-checked:after:bg-on-primary-dark dark:peer-focus:outline-outline-dark-strong dark:peer-focus:peer-checked:outline-primary-dark" aria-hidden="true"></div>
</label>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
{# Adapted from the vendored Penguin UI component {# Adapted from the vendored Penguin UI component
(assets/views/penguinui/card/ecommerce-product-card.html): (penguinui-components/card/ecommerce-product-card.html):
wired to our product data + i18n + htmx add-to-cart + toast. The demo rating wired to our product data + i18n + htmx add-to-cart + toast. The demo rating
stars, hardcoded title/price/description/image and the `max-w-sm` (which fights stars, hardcoded title/price/description/image and the `max-w-sm` (which fights
the shop grid) are dropped; the whole card links to the product page. #} the shop grid) are dropped; the whole card links to the product page. #}
@@ -28,13 +28,7 @@
hx-on::after-request="if (event.detail.successful) toast('{{ t(key='cart-added', lang=lang | default(value='sk')) }}')"> hx-on::after-request="if (event.detail.successful) toast('{{ t(key='cart-added', lang=lang | default(value='sk')) }}')">
<input type="hidden" name="product_id" value="{{ product.id }}"> <input type="hidden" name="product_id" value="{{ product.id }}">
<input type="hidden" name="quantity" value="1"> <input type="hidden" name="quantity" value="1">
<button type="submit" {{ ui::button(label=t(key="add-to-cart", lang=lang | default(value='sk')), type="submit", extra="w-full", icon='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-3.5"><path fill-rule="evenodd" d="M5 4a3 3 0 0 1 6 0v1h.643a1.5 1.5 0 0 1 1.492 1.35l.7 7A1.5 1.5 0 0 1 12.342 15H3.657a1.5 1.5 0 0 1-1.492-1.65l.7-7A1.5 1.5 0 0 1 4.357 5H5V4Zm4.5 0v1h-3V4a1.5 1.5 0 0 1 3 0Zm-3 3.75a.75.75 0 0 0-1.5 0v1a3 3 0 1 0 6 0v-1a.75.75 0 0 0-1.5 0v1a1.5 1.5 0 1 1-3 0v-1Z" clip-rule="evenodd" /></svg>') }}
class="flex w-full items-center justify-center gap-2 whitespace-nowrap rounded-radius bg-primary px-4 py-2 text-center text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 dark:bg-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-3.5">
<path fill-rule="evenodd" d="M5 4a3 3 0 0 1 6 0v1h.643a1.5 1.5 0 0 1 1.492 1.35l.7 7A1.5 1.5 0 0 1 12.342 15H3.657a1.5 1.5 0 0 1-1.492-1.65l.7-7A1.5 1.5 0 0 1 4.357 5H5V4Zm4.5 0v1h-3V4a1.5 1.5 0 0 1 3 0Zm-3 3.75a.75.75 0 0 0-1.5 0v1a3 3 0 1 0 6 0v-1a.75.75 0 0 0-1.5 0v1a1.5 1.5 0 1 1-3 0v-1Z" clip-rule="evenodd" />
</svg>
{{ t(key="add-to-cart", lang=lang | default(value='sk')) }}
</button>
</form> </form>
{% else %} {% else %}
<p class="inline-flex justify-center rounded-radius bg-danger/10 px-3 py-2 text-xs font-medium text-danger">{{ t(key="out-of-stock", lang=lang | default(value='sk')) }}</p> <p class="inline-flex justify-center rounded-radius bg-danger/10 px-3 py-2 text-xs font-medium text-danger">{{ t(key="out-of-stock", lang=lang | default(value='sk')) }}</p>

View File

@@ -3,18 +3,18 @@
and returned on its own by /cart/update and /cart/remove. #} and returned on its own by /cart/update and /cart/remove. #}
{% import "macros/ui.html" as ui %} {% import "macros/ui.html" as ui %}
{% if items | length > 0 %} {% if items | length > 0 %}
<div class="overflow-hidden rounded-radius border border-outline dark:border-outline-dark"> <div class="{{ ui::table_wrap_cls() }}">
<table class="w-full text-left text-sm"> <table class="{{ ui::table_cls() }}">
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70"> <thead class="{{ ui::thead_cls() }}">
<tr> <tr>
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 font-semibold">{{ t(key="price", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="price", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 font-semibold">{{ t(key="quantity", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="quantity", lang=lang | default(value='sk'))) }}
<th class="px-4 py-3 text-right font-semibold">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</th> {{ ui::th(label=t(key="cart-total", lang=lang | default(value='sk')), align="text-right") }}
<th class="px-4 py-3"></th> {{ ui::th(label="") }}
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-outline dark:divide-outline-dark"> <tbody class="{{ ui::tbody_cls() }}">
{% for item in items %} {% for item in items %}
<tr> <tr>
<td class="px-4 py-3"> <td class="px-4 py-3">
@@ -36,7 +36,7 @@
$el.dispatchEvent(new Event('cartchange', { bubbles: true })); $el.dispatchEvent(new Event('cartchange', { bubbles: true }));
} }
" "
class="w-20 rounded-radius border border-outline bg-surface px-2 py-1 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark"> class="w-20 rounded-radius border border-outline bg-surface-alt px-2 py-1 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark">
</form> </form>
</td> </td>
<td class="px-4 py-3 text-right font-medium tabular-nums">{{ item.line_total }} {{ item.currency }}</td> <td class="px-4 py-3 text-right font-medium tabular-nums">{{ item.line_total }} {{ item.currency }}</td>
@@ -44,13 +44,13 @@
<form method="post" action="/cart/remove" <form method="post" action="/cart/remove"
hx-post="/cart/remove" hx-target="#cart-body" hx-swap="innerHTML"> hx-post="/cart/remove" hx-target="#cart-body" hx-swap="innerHTML">
<input type="hidden" name="product_id" value="{{ item.id }}"> <input type="hidden" name="product_id" value="{{ item.id }}">
<button type="submit" class="text-xs font-medium text-danger hover:underline">{{ t(key="cart-remove", lang=lang | default(value='sk')) }}</button> {{ ui::button(variant="ghost-danger", label=t(key="cart-remove", lang=lang | default(value='sk')), type="submit", size="px-2 py-1 text-xs") }}
</form> </form>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot class="border-t border-outline dark:border-outline-dark"> <tfoot class="{{ ui::tfoot_cls() }}">
<tr> <tr>
<td colspan="3" class="px-4 py-3 text-right font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</td> <td colspan="3" class="px-4 py-3 text-right font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</td>
<td class="px-4 py-3 text-right text-lg font-bold tabular-nums text-primary dark:text-primary-dark">{{ total }} {{ currency }}</td> <td class="px-4 py-3 text-right text-lg font-bold tabular-nums text-primary dark:text-primary-dark">{{ total }} {{ currency }}</td>
@@ -61,12 +61,12 @@
</div> </div>
<div class="mt-6 flex flex-wrap justify-between gap-3"> <div class="mt-6 flex flex-wrap justify-between gap-3">
{{ ui::button_outline(label=t(key="cart-continue", lang=lang | default(value='sk')), href="/shop") }} {{ ui::button(variant="outline-secondary", label=t(key="cart-continue", lang=lang | default(value='sk')), href="/shop") }}
{{ ui::button_primary(label=t(key="cart-checkout", lang=lang | default(value='sk')), href="/checkout") }} {{ ui::button(label=t(key="cart-checkout", lang=lang | default(value='sk')), href="/checkout", size="px-5 py-2 text-sm") }}
</div> </div>
{% else %} {% else %}
<div class="rounded-radius border border-outline px-6 py-16 text-center dark:border-outline-dark"> <div class="rounded-radius border border-outline px-6 py-16 text-center dark:border-outline-dark">
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="cart-empty", lang=lang | default(value='sk')) }}</p> <p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="cart-empty", lang=lang | default(value='sk')) }}</p>
{{ ui::button_primary(label=t(key="cart-continue", lang=lang | default(value='sk')), href="/shop", extra="mt-4") }} {{ ui::button(label=t(key="cart-continue", lang=lang | default(value='sk')), href="/shop", extra="mt-4") }}
</div> </div>
{% endif %} {% endif %}

View File

@@ -7,7 +7,7 @@
one of its subcategories. one of its subcategories.
Adapted from the vendored Penguin UI component Adapted from the vendored Penguin UI component
assets/views/penguinui/sidebar/sidebar-with-collapsible-menus.html: Penguin's penguinui-components/sidebar/sidebar-with-collapsible-menus.html: Penguin's
link treatment + active state + chevron-down rotation. Deviations: the group link treatment + active state + chevron-down rotation. Deviations: the group
row keeps our link + toggle split (categories are navigable, not just row keeps our link + toggle split (categories are navigable, not just
expandable), and we use x-show/x-transition instead of upstream's x-collapse expandable), and we use x-show/x-transition instead of upstream's x-collapse

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ category.name }}{% endblock title %} {% block title %}{{ category.name }}{% endblock title %}

View File

@@ -11,6 +11,7 @@
<form method="post" action="/checkout" hx-boost="false" <form method="post" action="/checkout" hx-boost="false"
x-data="{ x-data="{
paymentMethod: '', paymentMethod: '',
accountType: '{{ prefill_account_type | default(value='personal') }}',
carrier: '', carrier: '',
carrierPrice: 0, carrierPrice: 0,
requiresPoint: false, requiresPoint: false,
@@ -31,25 +32,73 @@
class="mt-6 grid gap-8 lg:grid-cols-3"> class="mt-6 grid gap-8 lg:grid-cols-3">
<div class="space-y-6 lg:col-span-2"> <div class="space-y-6 lg:col-span-2">
<!-- personal vs company. Fixed (read-only) for a logged-in account; a guest
picks it and the choice will type any account they create. -->
<fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-type", lang=lang | default(value='sk')) }}</legend>
{% if account_fixed %}
<div class="flex items-center gap-2">
{% if prefill_account_type == "company" %}
{{ ui::badge(label=t(key="account-company", lang=lang | default(value='sk')), variant="primary") }}
{% else %}
{{ ui::badge(label=t(key="account-personal", lang=lang | default(value='sk')), variant="neutral") }}
{% endif %}
<span class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-type-locked", lang=lang | default(value='sk')) }}</span>
</div>
{% else %}
<div class="grid gap-3 sm:grid-cols-2">
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
{{ ui::radio(name="account_type", value="personal", attrs='x-model="accountType"') }}
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-personal", lang=lang | default(value='sk')) }}</span>
</label>
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
{{ ui::radio(name="account_type", value="company", attrs='x-model="accountType"') }}
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company", lang=lang | default(value='sk')) }}</span>
</label>
</div>
{% endif %}
</fieldset>
<!-- company billing details (company accounts only) -->
<fieldset x-show="accountType === 'company'" x-cloak class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company-details", lang=lang | default(value='sk')) }}</legend>
<div class="space-y-1.5">
<label for="company_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-name", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
{{ ui::input(name="company_name", id="company_name", value=prefill_company_name | default(value=''), autocomplete="organization") }}
</div>
<div class="grid gap-4 sm:grid-cols-3">
<div class="space-y-1.5">
<label for="company_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-ico", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
{{ ui::input(name="company_id", id="company_id", value=prefill_company_id | default(value='')) }}
</div>
<div class="space-y-1.5">
<label for="tax_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-dic", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
{{ ui::input(name="tax_id", id="tax_id", value=prefill_tax_id | default(value='')) }}
</div>
<div class="space-y-1.5">
<label for="vat_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-icdph", lang=lang | default(value='sk')) }} <span class="text-on-surface/50 dark:text-on-surface-dark/50">({{ t(key="field-optional", lang=lang | default(value='sk')) }})</span></label>
{{ ui::input(name="vat_id", id="vat_id", value=prefill_vat_id | default(value='')) }}
</div>
</div>
</fieldset>
<!-- contact --> <!-- contact -->
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt"> <fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend> <legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="email" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-email", lang=lang | default(value='sk')) }}</label> <label for="email" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-email", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
<input id="email" name="email" type="email" required {{ ui::input(name="email", id="email", type="email", value=prefill_email | default(value=''), required=true, autocomplete="email") }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="customer_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-name", lang=lang | default(value='sk')) }}</label> <label for="customer_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-name", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
<input id="customer_name" name="customer_name" type="text" required {{ ui::input(name="customer_name", id="customer_name", value=prefill_name | default(value=''), required=true, autocomplete="name") }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="phone" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-phone", lang=lang | default(value='sk')) }}</label> <label for="phone" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-phone", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
<div class="flex gap-2"> <div class="flex gap-2">
<!-- editable combobox: type freely or pick from the dropdown --> <!-- editable combobox: type freely or pick from the dropdown -->
<div class="relative w-28 shrink-0" @click.outside="prefixOpen = false" <div class="relative w-28 shrink-0" @click.outside="prefixOpen = false"
x-data="{ prefixOpen: false, prefix: '+421', opts: [ x-data="{ prefixOpen: false, prefix: '{{ prefill_phone_prefix | default(value='+421') }}', opts: [
{ v: '+421', l: '🇸🇰 +421' }, { v: '+420', l: '🇨🇿 +420' }, { v: '+421', l: '🇸🇰 +421' }, { v: '+420', l: '🇨🇿 +420' },
{ v: '+43', l: '🇦🇹 +43' }, { v: '+49', l: '🇩🇪 +49' }, { v: '+43', l: '🇦🇹 +43' }, { v: '+49', l: '🇩🇪 +49' },
{ v: '+48', l: '🇵🇱 +48' }, { v: '+36', l: '🇭🇺 +36' }, { v: '+48', l: '🇵🇱 +48' }, { v: '+36', l: '🇭🇺 +36' },
@@ -73,8 +122,7 @@
</template> </template>
</ul> </ul>
</div> </div>
<input id="phone" name="phone" type="tel" required autocomplete="tel" inputmode="tel" placeholder="900 000 000" {{ ui::input(name="phone", id="phone", type="tel", value=prefill_phone | default(value=''), required=true, autocomplete="tel", placeholder="900 000 000", attrs='inputmode="tel"') }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
</div> </div>
</fieldset> </fieldset>
@@ -83,25 +131,22 @@
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt"> <fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</legend> <legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</legend>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="address" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-address", lang=lang | default(value='sk')) }}</label> <label for="address" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-address", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
<input id="address" name="address" type="text" required {{ ui::input(name="address", id="address", value=prefill_address | default(value=''), required=true, autocomplete="street-address") }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="grid gap-4 sm:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-3">
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="city" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-city", lang=lang | default(value='sk')) }}</label> <label for="city" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-city", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
<input id="city" name="city" type="text" required {{ ui::input(name="city", id="city", value=prefill_city | default(value=''), required=true, autocomplete="address-level2") }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="zip" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-zip", lang=lang | default(value='sk')) }}</label> <label for="zip" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-zip", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
<input id="zip" name="zip" type="text" required {{ ui::input(name="zip", id="zip", value=prefill_zip | default(value=''), required=true, autocomplete="postal-code") }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="country" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-country", lang=lang | default(value='sk')) }}</label> <label for="country" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-country", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
<div class="relative" @click.outside="countryOpen = false" <div class="relative" @click.outside="countryOpen = false"
x-data="{ countryOpen: false, country: '{{ t(key='country-sk', lang=lang | default(value='sk')) }}', opts: [ x-data="{ countryOpen: false, country: '{{ prefill_country | default(value=t(key='country-sk', lang=lang | default(value='sk'))) }}', opts: [
{ v: '{{ t(key='country-sk', lang=lang | default(value='sk')) }}', l: '🇸🇰 {{ t(key='country-sk', lang=lang | default(value='sk')) }}' }, { v: '{{ t(key='country-sk', lang=lang | default(value='sk')) }}', l: '🇸🇰 {{ t(key='country-sk', lang=lang | default(value='sk')) }}' },
{ v: '{{ t(key='country-cz', lang=lang | default(value='sk')) }}', l: '🇨🇿 {{ t(key='country-cz', lang=lang | default(value='sk')) }}' }, { v: '{{ t(key='country-cz', lang=lang | default(value='sk')) }}', l: '🇨🇿 {{ t(key='country-cz', lang=lang | default(value='sk')) }}' },
{ v: '{{ t(key='country-at', lang=lang | default(value='sk')) }}', l: '🇦🇹 {{ t(key='country-at', lang=lang | default(value='sk')) }}' }, { v: '{{ t(key='country-at', lang=lang | default(value='sk')) }}', l: '🇦🇹 {{ t(key='country-at', lang=lang | default(value='sk')) }}' },
@@ -132,13 +177,14 @@
<!-- carrier --> <!-- carrier -->
<fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt"> <fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-carrier", lang=lang | default(value='sk')) }}</legend> <legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-carrier", lang=lang | default(value='sk')) }}{{ ui::req() }}</legend>
{% for m in shipping_methods %} {% for m in shipping_methods %}
<label class="flex cursor-pointer items-center justify-between gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark"> <label class="flex cursor-pointer items-center justify-between gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
<span class="flex items-center gap-3"> <span class="flex items-center gap-3">
<!-- Penguin radio dot inline (the @change mixes nested single+double quotes, can't pass through a Tera macro arg) -->
<input type="radio" name="carrier_code" value="{{ m.code }}" required <input type="radio" name="carrier_code" value="{{ m.code }}" required
@change="carrier='{{ m.code }}'; carrierPrice={{ m.price_cents }}; requiresPoint={{ m.requires_pickup_point }}; pointId=''; pointName=''" @change="carrier='{{ m.code }}'; carrierPrice={{ m.price_cents }}; requiresPoint={{ m.requires_pickup_point }}; pointId=''; pointName=''"
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark"> class="before:content[''] relative h-4 w-4 appearance-none rounded-full border border-outline bg-surface before:invisible before:absolute before:left-1/2 before:top-1/2 before:h-1.5 before:w-1.5 before:-translate-x-1/2 before:-translate-y-1/2 before:rounded-full before:bg-on-primary checked:border-primary checked:bg-primary checked:before:visible focus:outline-2 focus:outline-offset-2 focus:outline-outline-strong checked:focus:outline-primary disabled:cursor-not-allowed dark:border-outline-dark dark:bg-surface-dark dark:before:bg-on-primary-dark dark:checked:border-primary-dark dark:checked:bg-primary-dark dark:focus:outline-outline-dark-strong dark:checked:focus:outline-primary-dark">
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ m.name }}</span> <span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ m.name }}</span>
</span> </span>
<span class="tabular-nums text-on-surface/80 dark:text-on-surface-dark/80">{{ m.price }} {{ currency }}</span> <span class="tabular-nums text-on-surface/80 dark:text-on-surface-dark/80">{{ m.price }} {{ currency }}</span>
@@ -167,24 +213,35 @@
<!-- payment --> <!-- payment -->
<fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt"> <fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-payment", lang=lang | default(value='sk')) }}</legend> <legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-payment", lang=lang | default(value='sk')) }}{{ ui::req() }}</legend>
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark"> <label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
<input type="radio" name="payment_method" value="cod" required x-model="paymentMethod" {{ ui::radio(name="payment_method", value="cod", attrs='required x-model="paymentMethod"') }}
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark">
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-cod", lang=lang | default(value='sk')) }}</span> <span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-cod", lang=lang | default(value='sk')) }}</span>
</label> </label>
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark"> <label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
<input type="radio" name="payment_method" value="bank_transfer" required x-model="paymentMethod" {{ ui::radio(name="payment_method", value="bank_transfer", attrs='required x-model="paymentMethod"') }}
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark">
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-bank", lang=lang | default(value='sk')) }}</span> <span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-bank", lang=lang | default(value='sk')) }}</span>
</label> </label>
</fieldset> </fieldset>
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="note" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-note", lang=lang | default(value='sk')) }}</label> <label for="note" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-note", lang=lang | default(value='sk')) }}</label>
<textarea id="note" name="note" rows="3" {{ ui::textarea(name="note", id="note", rows="3") }}
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark"></textarea>
</div> </div>
{% if logged_in_customer and not profile_filled %}
<!-- offered only when the profile has no saved address yet; if it was filled
in advance we leave it untouched -->
{{ ui::checkbox(name="save_profile", id="save_profile", label=t(key="checkout-save-profile", lang=lang | default(value='sk'))) }}
{% endif %}
{% if can_create_account %}
<!-- guests may turn this order into an account (typed by their choice above) -->
<div class="space-y-1.5 rounded-radius border border-outline bg-surface p-4 dark:border-outline-dark dark:bg-surface-dark-alt">
{{ ui::checkbox(name="create_account", id="create_account", label=t(key="checkout-create-account", lang=lang | default(value='sk'))) }}
<p class="pl-6 text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="checkout-create-account-hint", lang=lang | default(value='sk')) }}</p>
</div>
{% endif %}
</div> </div>
<!-- summary --> <!-- summary -->
@@ -212,7 +269,7 @@
<span>{{ t(key="cart-total", lang=lang | default(value='sk')) }}</span> <span>{{ t(key="cart-total", lang=lang | default(value='sk')) }}</span>
<span class="tabular-nums text-primary dark:text-primary-dark" x-text="fmt(subtotal + carrierPrice) + ' {{ currency }}'"></span> <span class="tabular-nums text-primary dark:text-primary-dark" x-text="fmt(subtotal + carrierPrice) + ' {{ currency }}'"></span>
</div> </div>
{{ ui::button_primary(label=t(key="checkout-place-order", lang=lang | default(value='sk')), type="submit", attrs=':disabled="!canSubmit"', extra="w-full disabled:opacity-40") }} {{ ui::button(label=t(key="checkout-place-order", lang=lang | default(value='sk')), type="submit", attrs=':disabled="!canSubmit"', extra="w-full", size="px-6 py-2.5 text-sm") }}
</aside> </aside>
</form> </form>
{% endblock content %} {% endblock content %}

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="nav-shop", lang=lang | default(value='sk')) }}{% endblock title %} {% block title %}{{ t(key="nav-shop", lang=lang | default(value='sk')) }}{% endblock title %}

View File

@@ -15,6 +15,12 @@
<p class="mt-1 text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-confirmed-sub", lang=lang | default(value='sk')) }}</p> <p class="mt-1 text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-confirmed-sub", lang=lang | default(value='sk')) }}</p>
</div> </div>
{% if account_created %}
<div class="rounded-radius border border-primary/40 bg-primary/5 p-4 text-sm text-on-surface dark:border-primary-dark/40 dark:text-on-surface-dark" role="status">
{{ t(key="order-account-created", lang=lang | default(value='sk')) }}
</div>
{% endif %}
<div class="rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt"> <div class="rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
<div class="flex flex-wrap justify-between gap-2 border-b border-outline pb-3 dark:border-outline-dark"> <div class="flex flex-wrap justify-between gap-2 border-b border-outline pb-3 dark:border-outline-dark">
<span class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-number", lang=lang | default(value='sk')) }}</span> <span class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-number", lang=lang | default(value='sk')) }}</span>
@@ -56,7 +62,7 @@
{% endif %} {% endif %}
<div class="text-center"> <div class="text-center">
{{ ui::button_outline(label=t(key="cart-continue", lang=lang | default(value='sk')), href="/shop") }} {{ ui::button(variant="outline-secondary", label=t(key="cart-continue", lang=lang | default(value='sk')), href="/shop", size="px-5 py-2 text-sm") }}
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@@ -5,14 +5,35 @@
{% block content %} {% block content %}
<div class="grid gap-10 lg:grid-cols-2"> <div class="grid gap-10 lg:grid-cols-2">
<!-- gallery --> <!-- gallery — prev/next arrows + opacity transitions adapted from
<div x-data="{ active: 0 }" class="space-y-4"> penguinui/carousel/default-carousel.html; kept our product thumbnail strip
<div class="aspect-square overflow-hidden rounded-radius border border-outline bg-surface-alt dark:border-outline-dark dark:bg-surface-dark-alt"> (more useful than carousel dots for a product) and 0-based `active` -->
<div x-data="{ active: 0, count: {{ images | length }},
prev() { this.active = this.active > 0 ? this.active - 1 : this.count - 1 },
next() { this.active = this.active < this.count - 1 ? this.active + 1 : 0 } }"
class="space-y-4">
<div class="relative aspect-square overflow-hidden rounded-radius border border-outline bg-surface-alt dark:border-outline-dark dark:bg-surface-dark-alt">
{% if images | length > 0 %} {% if images | length > 0 %}
{% for image in images %} {% for image in images %}
<img x-show="active === {{ loop.index0 }}" src="/images/{{ image }}" alt="{{ product.name }}" class="size-full object-cover"> <img x-show="active === {{ loop.index0 }}" x-transition.opacity.duration.300ms src="/images/{{ image }}" alt="{{ product.name }}" class="absolute inset-0 size-full object-cover">
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if images | length > 1 %}
<!-- previous slide -->
<button type="button" @click="prev()" aria-label="{{ t(key='gallery-prev', lang=lang | default(value='sk')) }}"
class="absolute left-3 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full bg-surface/40 p-2 text-on-surface transition hover:bg-surface/60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:bg-surface-dark/40 dark:text-on-surface-dark dark:hover:bg-surface-dark/60 dark:focus-visible:outline-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="3" class="size-5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
</button>
<!-- next slide -->
<button type="button" @click="next()" aria-label="{{ t(key='gallery-next', lang=lang | default(value='sk')) }}"
class="absolute right-3 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full bg-surface/40 p-2 text-on-surface transition hover:bg-surface/60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:bg-surface-dark/40 dark:text-on-surface-dark dark:hover:bg-surface-dark/60 dark:focus-visible:outline-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="3" class="size-5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
{% endif %}
</div> </div>
{% if images | length > 1 %} {% if images | length > 1 %}
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
@@ -45,10 +66,9 @@
<input type="hidden" name="product_id" value="{{ product.id }}"> <input type="hidden" name="product_id" value="{{ product.id }}">
<div class="space-y-1.5"> <div class="space-y-1.5">
<label for="quantity" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="quantity", lang=lang | default(value='sk')) }}</label> <label for="quantity" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="quantity", lang=lang | default(value='sk')) }}</label>
<input id="quantity" name="quantity" type="number" min="1" max="{{ product.stock }}" value="1" {{ ui::input(name="quantity", id="quantity", type="number", value="1", width="w-24", attrs='min="1" max="' ~ product.stock ~ '"') }}
class="w-24 rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
</div> </div>
{{ ui::button_primary(label=t(key="add-to-cart", lang=lang | default(value='sk')), type="submit") }} {{ ui::button(label=t(key="add-to-cart", lang=lang | default(value='sk')), type="submit", size="px-5 py-2 text-sm") }}
</form> </form>
<p class="text-sm text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="in-stock", lang=lang | default(value='sk')) }}: {{ product.stock }}</p> <p class="text-sm text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="in-stock", lang=lang | default(value='sk')) }}: {{ product.stock }}</p>
{% else %} {% else %}

24
config/casbin/model.conf Normal file
View File

@@ -0,0 +1,24 @@
# Casbin access model for the storefront.
#
# Request is (subject, object, action) = (role, request-path, HTTP-method);
# axum-casbin supplies path + method automatically and the subject comes from
# our JWT-derived CasbinVals (see src/shared/rbac.rs).
#
# Deny-override: every request is allowed unless a matching policy line marks it
# `deny`. That keeps the public storefront fully open and lets the policy file
# carve out the protected `/admin/*` subtree for non-admins only.
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act, eft
[role_definition]
g = _, _
[policy_effect]
e = !some(where (p.eft == deny))
[matchers]
m = (r.sub == p.sub || g(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)

25
config/casbin/policy.csv Normal file
View File

@@ -0,0 +1,25 @@
# Authorization policy. Format: p, subject(role), object(path-pattern), action(method-regex), effect
#
# DECISION: this app intentionally runs with a SINGLE hardcoded admin (the user
# whose email matches ADMIN_EMAIL in .env, via guard::is_admin / admin_seeder).
# Everyone else is `customer` (logged in) or `anonymous` (not). There is no
# stored `role` column yet. This is a deliberate choice for current scale, not a
# limitation of the wiring — see src/shared/rbac.rs for the upgrade path.
#
# Deny everyone except admins under the admin subtree. `keyMatch` treats the
# trailing `*` as "anything after /admin/", so /admin/dashboard, /admin/orders/5
# etc. are all covered; the bare /admin entry point stays open so it can redirect
# to /login. Admins match no deny rule and so are allowed through.
#
# To grow this: give users a real role, emit it as the subject in
# src/shared/rbac.rs, then add `p` lines (allow/deny) and `g, <user>, <role>`
# mappings here.
p, customer, /admin/*, .*, deny
p, anonymous, /admin/*, .*, deny
# Admin-only endpoints that live outside the /admin/* subtree: the admin JSON
# API and the image upload. Public image serving (/images/{filename}) is GET and
# not matched here, so it stays open.
p, customer, /api/admin/*, .*, deny
p, anonymous, /api/admin/*, .*, deny
p, customer, /images/upload, .*, deny
p, anonymous, /images/upload, .*, deny
Can't render this file because it has a wrong number of fields in line 2.

View File

@@ -45,19 +45,24 @@ workers:
# Mailer Configuration. # Mailer Configuration.
mailer: mailer:
# SMTP mailer configuration. # SMTP mailer configuration. Defaults target a local catcher (MailHog/Mailpit
# on localhost:1025); set the SMTP_* env vars to point at a real server. The
# auth block is only emitted when SMTP_PASSWORD is provided, so the secret is
# never stored here — pass it in at launch (e.g. from `pass`).
smtp: smtp:
# Enable/Disable smtp mailer. # Enable/Disable smtp mailer.
enable: true enable: {{ get_env(name="SMTP_ENABLE", default="true") }}
# SMTP server host. e.x localhost, smtp.gmail.com # SMTP server host. e.x localhost, smtp.gmail.com
host: localhost host: "{{ get_env(name="SMTP_HOST", default="localhost") }}"
# SMTP server port # SMTP server port
port: 1025 port: {{ get_env(name="SMTP_PORT", default="1025") }}
# Use secure connection (SSL/TLS). # Use secure connection (SSL/TLS).
secure: false secure: {{ get_env(name="SMTP_SECURE", default="false") }}
# auth: {% if get_env(name="SMTP_PASSWORD", default="") != "" %}
# user: auth:
# password: user: "{{ get_env(name="SMTP_USER", default="") }}"
password: "{{ get_env(name="SMTP_PASSWORD", default="") }}"
{% endif %}
# Override the SMTP hello name (default is the machine's hostname) # Override the SMTP hello name (default is the machine's hostname)
# hello_name: # hello_name:
@@ -125,3 +130,30 @@ settings:
# Bank-transfer payment details shown on the order confirmation. # Bank-transfer payment details shown on the order confirmation.
bank_iban: {{ get_env(name="BANK_IBAN", default="SK00 0000 0000 0000 0000 0000") }} bank_iban: {{ get_env(name="BANK_IBAN", default="SK00 0000 0000 0000 0000 0000") }}
bank_account_name: {{ get_env(name="BANK_ACCOUNT_NAME", default="Kompress s.r.o.") }} bank_account_name: {{ get_env(name="BANK_ACCOUNT_NAME", default="Kompress s.r.o.") }}
# loco-oauth2: social login. Credentials come from .env (create them in the
# Google Cloud console and register the redirect_url below as an authorized
# redirect URI). Until OAUTH_CLIENT_ID/SECRET are set, the "Continue with
# Google" button will fail at the consent screen — the rest of auth is unaffected.
initializers:
oauth2:
# Key for the loco-oauth2 private cookie jar (>= 64 bytes). Override in prod.
secret_key: {{ get_env(name="OAUTH_PRIVATE_KEY", default="144, 76, 183, 1, 15, 184, 233, 174, 214, 251, 190, 186, 122, 61, 74, 84, 225, 110, 189, 115, 10, 251, 133, 128, 52, 46, 15, 66, 85, 1, 245, 73, 27, 113, 189, 15, 209, 205, 61, 100, 73, 31, 18, 58, 235, 105, 141, 36, 70, 92, 231, 151, 27, 32, 243, 117, 30, 244, 110, 89, 233, 196, 137, 130") }}
authorization_code:
- client_identifier: google
client_credentials:
client_id: {{ get_env(name="OAUTH_CLIENT_ID", default="oauth_client_id") }}
client_secret: {{ get_env(name="OAUTH_CLIENT_SECRET", default="oauth_client_secret") }}
url_config:
auth_url: {{ get_env(name="OAUTH_AUTH_URL", default="https://accounts.google.com/o/oauth2/auth") }}
token_url: {{ get_env(name="OAUTH_TOKEN_URL", default="https://www.googleapis.com/oauth2/v3/token") }}
redirect_url: {{ get_env(name="OAUTH_REDIRECT_URL", default="http://localhost:5150/api/oauth2/google/callback/cookie") }}
profile_url: {{ get_env(name="OAUTH_PROFILE_URL", default="https://openidconnect.googleapis.com/v1/userinfo") }}
scopes:
- "https://www.googleapis.com/auth/userinfo.email"
- "https://www.googleapis.com/auth/userinfo.profile"
cookie_config:
# After loco-oauth2 sets its session cookie it redirects here, where we
# mint our own auth_token cookie (see controllers/oauth2.rs::complete).
protected_url: {{ get_env(name="OAUTH_PROTECTED_URL", default="http://localhost:5150/api/oauth2/protected") }}
timeout_seconds: 600

View File

@@ -55,3 +55,25 @@ auth:
settings: settings:
admin_email: "{{ get_env(name="ADMIN_EMAIL", default="") }}" admin_email: "{{ get_env(name="ADMIN_EMAIL", default="") }}"
uploads_root: "{{ get_env(name="UPLOADS_ROOT", default="data/uploads") }}" uploads_root: "{{ get_env(name="UPLOADS_ROOT", default="data/uploads") }}"
# loco-oauth2 social login. All values must come from the environment in prod;
# OAUTH_REDIRECT_URL / OAUTH_PROTECTED_URL must use the real public origin.
initializers:
oauth2:
secret_key: "{{ get_env(name="OAUTH_PRIVATE_KEY") }}"
authorization_code:
- client_identifier: google
client_credentials:
client_id: "{{ get_env(name="OAUTH_CLIENT_ID") }}"
client_secret: "{{ get_env(name="OAUTH_CLIENT_SECRET") }}"
url_config:
auth_url: "{{ get_env(name="OAUTH_AUTH_URL", default="https://accounts.google.com/o/oauth2/auth") }}"
token_url: "{{ get_env(name="OAUTH_TOKEN_URL", default="https://www.googleapis.com/oauth2/v3/token") }}"
redirect_url: "{{ get_env(name="OAUTH_REDIRECT_URL") }}"
profile_url: "{{ get_env(name="OAUTH_PROFILE_URL", default="https://openidconnect.googleapis.com/v1/userinfo") }}"
scopes:
- "https://www.googleapis.com/auth/userinfo.email"
- "https://www.googleapis.com/auth/userinfo.profile"
cookie_config:
protected_url: "{{ get_env(name="OAUTH_PROTECTED_URL") }}"
timeout_seconds: 600

View File

@@ -42,33 +42,58 @@ workers:
# Mailer Configuration. # Mailer Configuration.
# Defaults keep the whole suite on the in-memory stub mailer. The real-SMTP
# smoke test (tests/mailer/smtp_send.rs) opts in by setting these env vars
# before boot; nothing else in the suite sends real mail.
mailer: mailer:
stub: true stub: {{ get_env(name="MAILER_STUB", default="true") }}
# SMTP mailer configuration. # SMTP mailer configuration.
smtp: smtp:
# Enable/Disable smtp mailer. # Enable/Disable smtp mailer.
enable: true enable: {{ get_env(name="SMTP_ENABLE", default="true") }}
# SMTP server host. e.x localhost, smtp.gmail.com # SMTP server host. e.x localhost, smtp.gmail.com
host: localhost host: "{{ get_env(name="SMTP_HOST", default="localhost") }}"
# SMTP server port # SMTP server port
port: 1025 port: {{ get_env(name="SMTP_PORT", default="1025") }}
# Use secure connection (SSL/TLS). # Use secure connection (SSL/TLS).
secure: false secure: {{ get_env(name="SMTP_SECURE", default="false") }}
# auth: auth:
# user: user: "{{ get_env(name="SMTP_USER", default="") }}"
# password: password: "{{ get_env(name="SMTP_PASSWORD", default="") }}"
# Initializers Configuration # Initializers Configuration
# initializers: # OAuth2StoreInitializer requires this block to boot (it builds the client store
# oauth2: # in after_routes). Static, non-secret placeholders: tests never perform a real
# authorization_code: # Authorization code grant type # OAuth2 handshake, they just need the store to construct successfully.
# - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config. initializers:
# ... other fields oauth2:
# Private-cookie key: a ", "-separated list of >=64 byte values (not a
# plain string). This is loco-oauth2's documented sample key; fine for tests.
secret_key: "144, 76, 183, 1, 15, 184, 233, 174, 214, 251, 190, 186, 122, 61, 74, 84, 225, 110, 189, 115, 10, 251, 133, 128, 52, 46, 15, 66, 85, 1, 245, 73, 27, 113, 189, 15, 209, 205, 61, 100, 73, 31, 18, 58, 235, 105, 141, 36, 70, 92, 231, 151, 27, 32, 243, 117, 30, 244, 110, 89, 233, 196, 137, 130"
authorization_code:
- client_identifier: google
client_credentials:
client_id: test-client-id
client_secret: test-client-secret
url_config:
auth_url: https://accounts.google.com/o/oauth2/auth
token_url: https://www.googleapis.com/oauth2/v3/token
redirect_url: http://localhost:5150/api/oauth2/google/callback
profile_url: https://openidconnect.googleapis.com/v1/userinfo
scopes:
- https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile
cookie_config:
protected_url: http://localhost:5150/
timeout_seconds: 600
# Database Configuration # Database Configuration
database: database:
# Database connection URI # Database connection URI. Pinned to the throwaway test DB and intentionally
uri: {{ get_env(name="DATABASE_URL", default="postgres://uni_loco_web_user:3@localhost:5432/kompress_eshop_test") }} # NOT read from `DATABASE_URL`: the app loads `.env` on boot (app.rs
# `load_config`), and this config has `dangerously_recreate: true`, so honoring
# an env override here would let `cargo test` recreate the dev/prod database.
uri: "postgres://uni_loco_web_user:3@localhost:5432/kompress_eshop_test"
# When enabled, the sql query will be logged. # When enabled, the sql query will be logged.
enable_logging: false enable_logging: false
# Set the timeout duration when acquiring a connection. # Set the timeout duration when acquiring a connection.

View File

@@ -0,0 +1,104 @@
# Google OAuth2 sign-in
"Continue with Google" on `/login` and `/register` is wired through
[`loco-oauth2`](https://github.com/yinho999/loco-oauth2). The code is complete
and compiles; this doc is the checklist to make the live flow work. Until the
credentials below are set, the button reaches Google and fails at the consent
screen — the rest of auth (password login, registration, verification) is
unaffected.
## How the flow works (for context)
1. User clicks **Continue with Google**`GET /api/oauth2/google` redirects to
Google's consent screen.
2. Google redirects back to `GET /api/oauth2/google/callback/cookie`.
loco-oauth2 exchanges the code, fetches the profile, upserts the user
(`OAuth2UserTrait::upsert_with_oauth`), stores an `o_auth2_sessions` row, sets
its own private session cookie, and redirects to `protected_url`.
3. `protected_url` = `GET /api/oauth2/protected` (our bridge,
`controllers/oauth2.rs::complete`). It mints **our** `auth_token` JWT cookie
and redirects: admins (email == `ADMIN_EMAIL`) → `/admin/dashboard`,
everyone else → `/`.
From there the user is a normal logged-in user (same JWT cookie as a password
login; the Casbin layer and guards treat them identically).
## 1. Create Google OAuth credentials
1. Go to <https://console.cloud.google.com/> → create/select a project.
2. **APIs & Services → OAuth consent screen**: configure it (External), add the
`.../auth/userinfo.email` and `.../auth/userinfo.profile` scopes, and add
your Google account as a **test user** while the app is in "Testing".
3. **APIs & Services → Credentials → Create Credentials → OAuth client ID**:
- Application type: **Web application**.
- **Authorized redirect URIs** — add exactly (must match the config's
`redirect_url`, no trailing slash):
- dev: `http://localhost:5150/api/oauth2/google/callback/cookie`
- prod: `https://YOUR_DOMAIN/api/oauth2/google/callback/cookie`
4. Copy the generated **Client ID** and **Client secret**.
## 2. Set environment variables (`.env`)
Read by `config/development.yaml``initializers.oauth2` (and the prod
equivalent). dotenvy loads `.env` on boot.
```bash
# Required
OAUTH_CLIENT_ID=xxxxxxxx.apps.googleusercontent.com
OAUTH_CLIENT_SECRET=xxxxxxxx
# Required in PRODUCTION (dev has working defaults)
OAUTH_PRIVATE_KEY="comma,separated,bytes >= 64 long" # key for loco-oauth2's private cookie jar
OAUTH_REDIRECT_URL=https://YOUR_DOMAIN/api/oauth2/google/callback/cookie
OAUTH_PROTECTED_URL=https://YOUR_DOMAIN/api/oauth2/protected
```
Notes:
- **dev** ships defaults for everything except `OAUTH_CLIENT_ID` /
`OAUTH_CLIENT_SECRET`, so locally you only need those two.
- `OAUTH_PRIVATE_KEY` must be ≥ 64 bytes (the dev default is a sample key — do
**not** reuse it in production). Generate a fresh one, e.g.
`python3 -c "import os;print(','.join(str(b) for b in os.urandom(64)))"`.
- `OAUTH_REDIRECT_URL` here and the Authorized redirect URI in the Google
console must be byte-for-byte identical.
## 3. Run / test
```bash
nix develop -c cargo loco start # MUST be inside nix develop (OpenSSL link, see memory)
```
- `auto_migrate: true` (dev) creates the `o_auth2_sessions` table on boot.
- Open `http://localhost:5150/login`**Continue with Google** → consent →
you should land back on `/` logged in (cart/nav reflect the session).
## 4. Production checklist
- [ ] Separate OAuth client (or at least the prod redirect URI) in Google.
- [ ] OAuth consent screen **published** (not just "Testing"), or real users
get blocked.
- [ ] `OAUTH_PRIVATE_KEY` set to a fresh ≥64-byte key (not the dev sample).
- [ ] `OAUTH_REDIRECT_URL` / `OAUTH_PROTECTED_URL` use the real `https://` origin.
- [ ] `server.host` / public origin correct so cookies + redirects resolve.
## Troubleshooting
| Symptom | Cause / fix |
|---|---|
| `redirect_uri_mismatch` at Google | Authorized redirect URI ≠ `OAUTH_REDIRECT_URL`. Make them identical (scheme, host, port, path, no trailing slash). |
| 403 / "access blocked: app not verified" | Add your account as a test user, or publish the consent screen. |
| `openssl-sys ... Could not find directory` at build | You ran `cargo` outside the dev shell. Use `nix develop -c cargo ...`. |
| Callback 500 / "could not create oauth2 store" | `initializers.oauth2` missing/invalid, or `OAUTH_PRIVATE_KEY` < 64 bytes. |
| Logged into Google but not into the app | The bridge (`/api/oauth2/protected`) didn't run check `protected_url` (`OAUTH_PROTECTED_URL`) points at it. |
## Where things live
- Config: `config/development.yaml` / `config/production.yaml`
`initializers.oauth2`
- Client store + session initializers: `src/initializers/oauth2.rs`,
`src/initializers/oauth2_session.rs`
- Routes + bridge handler: `src/controllers/oauth2.rs`
- User upsert (random password per advisory LOC-2025-04): `src/models/users.rs`
(`OAuth2UserTrait`)
- Session table: `src/models/o_auth2_sessions.rs` +
`migration/.../m20260618_000001_o_auth2_sessions.rs`

View File

@@ -86,7 +86,10 @@
buildInputs = [ buildInputs = [
rust rust
pkgs.pkg-config pkgs.pkg-config
# OpenSSL for crypto dependencies (loco-oauth2 -> oauth2/reqwest
# use native-tls); .dev provides headers + pkg-config metadata.
pkgs.openssl pkgs.openssl
pkgs.openssl.dev
pkgs.cmake pkgs.cmake
pkgs.llvmPackages.clang pkgs.llvmPackages.clang
pkgs.llvmPackages.libclang.lib pkgs.llvmPackages.libclang.lib

View File

@@ -5,20 +5,35 @@
## Vendoring convention ## Vendoring convention
When a Penguin UI component can replace a handcoded one, we vendor its source The full library is now vendored locally at repo-root
and then use it (instead of hand-rolling): `penguinui-components/` (177 component `.html` files, moved there
2026-06-18). Read components from there — **NO** network/curl/WebFetch
needed anymore.
1. Copy the component's source **byte-for-byte** from the [Penguin UI repo](https://github.com/SalarHoushvand/penguinui-components/tree/main) ### HARD RULE — read-only, never edit
into `assets/views/penguinui/`, **mirroring the upstream repo hierarchy**
(e.g. `toast-notification/stacking-toast-notification.html`). This directory `penguinui-components/` is a read-only third-party library, **NOT our
is reserved exclusively for vendored Penguin UI components and is kept an code**. Never edit, never `{% include %}`, never adapt in place. It is
**exact, unmodified mirror** of upstream — demo triggers, bugs and all. It's reference only; **copy markup OUT** of it and adapt at the use-site.
a reference, not the rendered markup.
2. Adapt it where it's actually used (strip docs-only demo triggers, fix obvious When a Penguin UI component can replace a handcoded one:
upstream bugs, wire data bindings). Note the deviations in a comment next to
the adapted copy. 1. Find the component in the local `penguinui-components/` directory.
3. Rebuild Tailwind (`make css`) so any new utility classes get compiled. 2. Copy its markup **out** into the appropriate `assets/views/` location,
4. Mark the section below as ✅ **DONE**. adapting it where used (strip docs-only demo triggers, fix obvious
upstream bugs, wire data bindings, map Penguin classes to our design
tokens). Note the deviations in a comment next to the adapted copy.
3. Keep the original `penguinui-components/` file **untouched** — it stays
as a byte-for-byte reference snapshot.
4. Rebuild Tailwind (`make css`) so any new utility classes get compiled.
5. Mark the section below as ✅ **DONE**.
### Why it's at repo root + the build guard
Moved OUT of `assets/views/` to repo root because Tailwind v4 auto-detects
sources from the project root — so `assets/css/app.css` carries
`@source not "../../penguinui-components";` to explicitly exclude it
from the build. If the build ever balloons, check that exclusion is intact.
--- ---
@@ -26,7 +41,7 @@ and then use it (instead of hand-rolling):
**Penguin UI: `toast-notification/stacking-toast-notification.html`** **Penguin UI: `toast-notification/stacking-toast-notification.html`**
- Exact upstream mirror at `assets/views/penguinui/toast-notification/stacking-toast-notification.html` (reference only) - Exact upstream mirror at `penguinui-components/toast-notification/stacking-toast-notification.html` (reference only)
- Adapted/rendered copy lives inline in `assets/views/base.html` (demo triggers - Adapted/rendered copy lives inline in `assets/views/base.html` (demo triggers
removed; the upstream dismiss-button `<svg>` quote bugs fixed) removed; the upstream dismiss-button `<svg>` quote bugs fixed)
- The global `toast('message')` JS helper now dispatches the component's - The global `toast('message')` JS helper now dispatches the component's
@@ -35,30 +50,42 @@ and then use it (instead of hand-rolling):
--- ---
## 1. Navbar ## 1. Navbar — ✅ DONE
**Penguin UI: `navbar/`** **Penguin UI: `navbar/default-navbar.html`**
| # | Location | What it is | Size | - Exact upstream mirror at `penguinui-components/navbar/default-navbar.html` (reference only)
|---|----------|------------|------| - **Link treatment** adopted from Penguin (matches the already-ported sidebars): the
| 1 | `assets/views/base.html:63-191` | Full site navbar: brand, desktop nav links, cart icon+badge, settings dropdown, mobile hamburger → mobile panel | ~130 lines | desktop nav links lost the pill-hover (`hover:bg-surface-alt` + `px-3 py-1.5`) for
| 2 | `assets/views/admin/base.html:102-114` | Admin top bar: hamburger toggle + breadcrumb text | ~13 lines | Penguin's text-only `underline-offset-2 hover:text-primary focus-visible:underline
focus:outline-hidden`, active (`aria-current=page`, set by `markActiveNav()` via
`data-nav`) = `font-semibold` + primary. Centralized into a `ui::nav_link(label,
href, data_nav, variant, attrs)` macro in `macros/ui.html` (variant ∈ default |
warning admin | danger). Logout stays an inline `<form><button>` (not an `<a>`).
- **Hamburger animation** adopted: both the site mobile-menu button and the admin
sidebar toggle now swap bars ↔ X (`x-show="!open"`/`x-show="open"`, Penguin X-path
`M6 18 18 6M6 6l12 12`), kept inside our ghost-square icon-button shell for
consistency with the cart/gear buttons.
- **Mobile menu panel**: kept our compact dropdown (better for this app's dense top
bar than Penguin's full-screen `fixed inset-x-0 top-0 pt-20` overlay, which would
cover the cart/settings/category-toggle). Items now use the sidebar menu-row
treatment (`hover:bg-primary/5`, underline focus) + `data-nav` so they show the
active state too.
- **Preserved intact** (the integration risks flagged here): cart icon + live
cookie-read badge, the `partials/settings_dropdown.html` include (language switcher
+ theme tristate), the mobile category-drawer toggle, and all Alpine toggles
(`mobile`, `cats`, `showSidebar`).
**Details for #1 (site navbar):** | # | Location | What it is |
- **Brand/logo**: `base.html:74-77` — plain `<a>` with text |---|----------|------------|
- **Desktop nav links**: `base.html:80-92``<ul>` with 45 items, manual `aria-current` routing | 1 | `assets/views/base.html` | Full site navbar (brand, links, cart badge, settings, mobile menu) |
- **Cart icon + badge**: `base.html:96-109` — hand-rolled SVG cart icon + an Alpine `x-data` badge that reads `document.cookie` directly | 2 | `assets/views/admin/base.html` | Admin top bar: animated hamburger + breadcrumb + settings |
- **Settings dropdown**: `base.html:110-162` — gear-icon trigger + language-switcher `<form>` + theme tristate (system/light/dark)
- **Mobile hamburger**: `base.html:164-172` — hamburger SVG button
- **Mobile menu panel**: `base.html:175-190` — dropdown `<ul>` with duplicated nav links
**Penguin navbar variants:** `default-navbar.html`, `with-call-to-action.html`, `with-search.html`, `with-user-profile.html`
--- ---
## 2. Sidebar (Admin) — ✅ DONE ## 2. Sidebar (Admin) — ✅ DONE
**Penguin UI: `sidebar/simple-sidebar.html`** **Penguin UI: `sidebar/simple-sidebar.html`**
- Exact upstream mirror at `assets/views/penguinui/sidebar/simple-sidebar.html` (reference only) - Exact upstream mirror at `penguinui-components/sidebar/simple-sidebar.html` (reference only)
- Adapted at use-site in `assets/views/admin/base.html`: the nav links + bottom - Adapted at use-site in `assets/views/admin/base.html`: the nav links + bottom
exit/logout now use Penguin's link treatment (`hover:bg-primary/5`, exit/logout now use Penguin's link treatment (`hover:bg-primary/5`,
`underline-offset-2 focus-visible:underline focus:outline-hidden`) and the `underline-offset-2 focus-visible:underline focus:outline-hidden`) and the
@@ -73,7 +100,7 @@ and then use it (instead of hand-rolling):
## 3. Sidebar (Category Accordion) — ✅ DONE ## 3. Sidebar (Category Accordion) — ✅ DONE
**Penguin UI: `sidebar/sidebar-with-collapsible-menus.html`** **Penguin UI: `sidebar/sidebar-with-collapsible-menus.html`**
- Exact upstream mirror at `assets/views/penguinui/sidebar/sidebar-with-collapsible-menus.html` (reference only) - Exact upstream mirror at `penguinui-components/sidebar/sidebar-with-collapsible-menus.html` (reference only)
- Adapted at use-site in `assets/views/shop/_sidebar.html`: Penguin link treatment + - Adapted at use-site in `assets/views/shop/_sidebar.html`: Penguin link treatment +
active state + chevron-down rotation (`rotate-180`); child items now sit in a active state + chevron-down rotation (`rotate-180`); child items now sit in a
bordered/indented list instead of the old `padding-left:28px` + `↳`. Kept our bordered/indented list instead of the old `padding-left:28px` + `↳`. Kept our
@@ -90,7 +117,7 @@ and then use it (instead of hand-rolling):
## 4. Dropdown (Settings) — ✅ DONE ## 4. Dropdown (Settings) — ✅ DONE
**Penguin UI: `dropdowns/dropdown-with-click.html`** **Penguin UI: `dropdowns/dropdown-with-click.html`**
- Exact upstream mirror at `assets/views/penguinui/dropdowns/dropdown-with-click.html` (reference only) - Exact upstream mirror at `penguinui-components/dropdowns/dropdown-with-click.html` (reference only)
- **De-duplicated**: the ~103-line copy-paste is now one shared partial - **De-duplicated**: the ~103-line copy-paste is now one shared partial
`assets/views/partials/settings_dropdown.html`, included by both `base.html` `assets/views/partials/settings_dropdown.html`, included by both `base.html`
and `admin/base.html` (each host keeps its own positioning wrapper and `admin/base.html` (each host keeps its own positioning wrapper
@@ -102,8 +129,19 @@ and then use it (instead of hand-rolling):
--- ---
## 5. Country / Phone Combobox ## 5. Country / Phone Combobox — ⛔ WON'T PORT (conscious deviation)
**Penguin UI: `text-input/` + custom dropdown list** **Penguin UI: `combobox/phone-number-input-with-country-code-dropdown.html`**
> **Decision 2026-06-18:** keep our lightweight hand-rolled comboboxes; do NOT
> port the Penguin one. The Penguin combobox depends on the **Alpine Focus
> plugin** (`x-trap`, `$focus.wrap().next()`) which this build does not bundle
> (same reason the settings dropdown & category accordion deviate), ships a
> 240-country `allOptions` list + `flagcdn.com` remote flag images, and a search
> field — far heavier than our deliberate 9-prefix / 6-country editable inputs.
> Our versions already use the Penguin design tokens (`bg-surface`, `border-outline`,
> `focus:outline-primary`) and emoji flags, so they look on-brand. Net: porting
> would add a JS dependency and external image loads for negative UX value.
> Revisit only if we adopt the Alpine Focus plugin project-wide.
| # | Location | What it is | Size | | # | Location | What it is | Size |
|---|----------|------------|------| |---|----------|------------|------|
@@ -125,7 +163,7 @@ and then use it (instead of hand-rolling):
## 6. Product Card — ✅ DONE ## 6. Product Card — ✅ DONE
**Penguin UI: `card/ecommerce-product-card.html`** **Penguin UI: `card/ecommerce-product-card.html`**
- Exact upstream mirror at `assets/views/penguinui/card/ecommerce-product-card.html` (reference only) - Exact upstream mirror at `penguinui-components/card/ecommerce-product-card.html` (reference only)
- Adapted/rendered copy is `assets/views/shop/_card.html`: `<article>` shell + Penguin - Adapted/rendered copy is `assets/views/shop/_card.html`: `<article>` shell + Penguin
image/title/price layout and the cart-icon add-to-cart button, wired to our product image/title/price layout and the cart-icon add-to-cart button, wired to our product
data + i18n + htmx `hx-post` add-to-cart + `toast()`. Demo-only rating stars, data + i18n + htmx `hx-post` add-to-cart + `toast()`. Demo-only rating stars,
@@ -134,135 +172,122 @@ and then use it (instead of hand-rolling):
--- ---
## 7. Product Image Gallery ## 7. Product Image Gallery — ✅ DONE
**Penguin UI: `carousel/` (3 variants)** **Penguin UI: `carousel/default-carousel.html`**
| # | Location | What it is | Size | - Exact upstream mirror at `penguinui-components/carousel/default-carousel.html` (reference only)
|---|----------|------------|------| - Adapted at use-site in `assets/views/shop/show.html`: added Penguin's overlay
| 12 | `assets/views/shop/show.html:8-26` | Image gallery with main image + thumbnail strip, Alpine `x-data="{ active: 0 }"` | ~19 lines | prev/next arrow buttons (`bg-surface/40` rounded, verbatim chevron SVGs) and
`x-transition.opacity.duration.300ms` fade between images. Added `prev()`/`next()`
**Details:** with wraparound to the gallery `x-data`; arrows + transitions only render when
- Main image: `x-show="active === {{ loop.index0 }}"` with `object-cover` `images | length > 1`.
- Thumbnail buttons: border changes to indicate active state - Deviations: kept our **product thumbnail strip** (more useful than carousel
- No transition/animation between images — just x-show toggling dot indicators for a product page) and our **0-based `active`** index (Penguin
uses 1-based `currentSlideIndex`); main images switched to `absolute inset-0`
so the fade cross-dissolves inside the `aspect-square` frame. New i18n keys
`gallery-prev`/`gallery-next` (sk + en) for the arrow `aria-label`s.
--- ---
## 8. Radio-Button Groups ## 8. Radio-Button Groups — ✅ DONE
**Penguin UI: radio (part of form inputs)** **Penguin UI: `radio/radio-with-container.html`**
| # | Location | What it is | Size | - Exact upstream mirror at `penguinui-components/radio/radio-with-container.html` (reference only)
|---|----------|------------|------| - New `ui::radio(name, value, id, checked, attrs, extra)` macro in `macros/ui.html`
| 13 | `assets/views/shop/checkout.html:133-165` | Carrier selection radio group (each option shows name + price) | ~33 lines | emits **only** the Penguin custom radio-dot `<input>` (verbatim `appearance-none`
| 14 | `assets/views/shop/checkout.html:167-180` | Payment method radio group (COD + bank transfer) | ~14 lines | + `before:` dot + `checked:bg-primary`). Callers keep their own card-style
`<label>` wrapper — we kept our `has-[:checked]:border-primary` card highlight,
**Details for #13:** which is richer than Penguin's plain `bg-surface-alt` container.
- `{% for m in shipping_methods %}` loop - Adopted at `shop/checkout.html`: both **payment** radios (`ui::radio` with
- Each `<label>` is a styled card with `has-[:checked]:border-primary` border highlight `attrs='required x-model="paymentMethod"'`). The **carrier** radio (in the
- Radio input triggers `@change` to update Alpine state (carrier, carrierPrice, requiresPoint) `{% for m in shipping_methods %}` loop) keeps the same Penguin dot class **inline**
- Pickup-point sub-panel shown via `x-show="requiresPoint"` because its `@change="carrier='{{ m.code }}'; …"` mixes nested single+double
quotes that can't pass through a Tera macro arg (same convention as the cart
**Details for #14:** qty input). Native `text-primary` radios are gone.
- Two hardcoded radio options: COD and bank_transfer
- `x-model="paymentMethod"` binding
--- ---
## 9. Checkbox ## 9. Checkbox — ✅ DONE
**Penguin UI: `checkbox/` (3 variants)** **Penguin UI: `checkbox/default-checkbox.html`**
| # | Location | What it is | Size | - Exact upstream mirror at `penguinui-components/checkbox/default-checkbox.html` (reference only)
|---|----------|------------|------| - `ui::checkbox(name, label, id, value="on", checked, attrs)` macro in `macros/ui.html`
| 15 | `assets/views/admin/catalog/product_form.html:85-89` | "Published" checkbox | ~5 lines | (full Penguin control: custom box + check-icon + label, `has-checked:`/`peer` variants).
| 16 | `assets/views/admin/catalog/category_form.html:67-71` | "Published" checkbox | ~5 lines | - Adopted: product/category "Published" + shipping "Enabled".
| 17 | `assets/views/admin/shipping/index.html:25-29` | "Enabled" checkbox | ~5 lines |
## 10. Text Input — ✅ DONE
**Penguin UI: `text-input/default-text-input.html`**
- Exact upstream mirror at `penguinui-components/text-input/default-text-input.html` (reference only)
- `ui::input(name, type, id, value, placeholder, required, autocomplete, attrs, extra, width="w-full")`
macro — **verbatim** Penguin classes (`bg-surface-alt`, `focus-visible:outline-*`).
Adopted at every text/email/number/password input: login (2), checkout (email,
name, phone, address, city, zip), product form (6), category form (3), product
detail quantity, shipping price (`width="w-28"`).
- The cart-body quantity input keeps its complex `@change` handler **inline** with
the same Penguin classes (mixed single/double quotes can't pass through a macro arg).
- Note: padding is Penguin's `px-2 py-2` (was `px-3`) and bg is `bg-surface-alt` (was
`bg-surface`) — the real Penguin look.
## 11. Textarea — ✅ DONE
**Penguin UI: `text-area/default-textarea.html`**
- Exact upstream mirror at `penguinui-components/text-area/default-textarea.html` (reference only)
- `ui::textarea(name, id, value, rows, placeholder, required, attrs, extra)` macro.
- Adopted: checkout note, product & category description.
## 12. Select/Dropdown (Native) — ✅ DONE
**Penguin UI: `select/default-select.html`**
- Exact upstream mirror at `penguinui-components/select/default-select.html` (reference only)
- Adopted inline (3 sites: product category, category parent, order status) — Penguin
`appearance-none` select on `bg-surface-alt` wrapped in `relative` with the chevron
SVG. Inline rather than a macro because the `<option>` set is caller-specific.
## 13. File Input — ✅ DONE
**Penguin UI: `file-input/default-file-input.html`**
- Exact upstream mirror at `penguinui-components/file-input/default-file-input.html` (reference only)
- `ui::file_input(name, id, accept, attrs, extra)` macro (verbatim Penguin `file:` styling).
- Adopted: product & category image upload.
--- ---
## 10. Text Input ## 14. Table — ✅ DONE
**Penguin UI: `text-input/` (8 variants)** **Penguin UI: `table/default-table.html`**
| # | Location | What it is | Size | - Exact upstream mirror at `penguinui-components/table/default-table.html` (reference only)
|---|----------|------------|------| - The same wrapper/thead/tbody/row/tfoot class structure was copy-pasted across all
| 18 | `assets/views/shop/checkout.html:37-44` | Email + name text inputs | ~8 lines | 5 tables (orders index, order detail, products, categories, cart body). Centralized
| 19 | `assets/views/shop/checkout.html:84-99` | Address, city, ZIP text inputs | ~16 lines | into **class-string macros** in `macros/ui.html`: `ui::table_wrap_cls()`,
| 20 | `assets/views/admin/login.html:34-51` | Email + password inputs (with focus ring styles) | ~18 lines | `ui::table_cls()`, `ui::thead_cls()`, `ui::tbody_cls()`, `ui::row_cls()` (hover),
| 21 | `assets/views/admin/catalog/product_form.html:19-68` | Name, price, currency, stock, SKU, slug inputs + textarea | ~50 lines | `ui::tfoot_cls()`, plus an element macro `ui::th(label, align="")` for header cells.
| 22 | `assets/views/admin/catalog/category_form.html:19-55` | Name, slug, position inputs + textarea | ~37 lines | - **Why class-string macros, not full row macros:** Tera has no slot/`{% raw %}{% call %}{% endraw %}`
| 23 | `assets/views/shop/show.html:46-48` | Quantity number input | ~3 lines | mechanism, and the cells are heterogeneous (product image+name, htmx quantity input
| 24 | `assets/views/shop/_cart_body.html:30-38` | Quantity number input with `@change` confirmation dialog | ~9 lines | with inline Alpine `@change`, badges, action-button forms), so rows stay inline. The
| 25 | `assets/views/admin/shipping/index.html:20-24` | Price text input | ~5 lines | macros centralize only the drift-prone chrome styling — `class="{{ ui::thead_cls() }}"`.
- **Penguin improvement adopted:** the wrapper now carries `w-full overflow-x-auto`
(from `default-table.html`) so wide tables scroll horizontally on mobile instead of
overflowing. Our `text-xs uppercase` thead + `px-4 py-3` cells were kept (deliberate,
richer than Penguin's `text-sm`/`p-4`).
- Interactive lists (orders/products/categories) use `ui::row_cls()` for the hover
highlight; non-interactive rows (order items, cart) omit it. `tfoot` (order detail +
cart totals) uses `ui::tfoot_cls()`.
**Pattern: Every input is hand-styled with:** | # | Location | What it is |
``` |---|----------|------------|
w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface | 34 | `assets/views/admin/orders/index.html` | Orders table |
focus:outline-2 focus:outline-primary | 35 | `assets/views/admin/orders/show.html` | Order items table + tfoot summary |
dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark | 36 | `assets/views/admin/catalog/products.html` | Products table (image+name, status pill, actions) |
``` | 37 | `assets/views/admin/catalog/categories.html` | Categories table (tree-indented, actions) |
This exact class string appears 15+ times across the codebase. | 38 | `assets/views/shop/_cart_body.html` | Cart table (htmx qty input, remove) + tfoot total |
---
## 11. Textarea
**Penguin UI: `textarea/`**
| # | Location | What it is | Size |
|---|----------|------------|------|
| 26 | `assets/views/shop/checkout.html:183-186` | Order note textarea | ~4 lines |
| 27 | `assets/views/admin/catalog/product_form.html:71-73` | Product description textarea | ~3 lines |
| 28 | `assets/views/admin/catalog/category_form.html:53-55` | Category description textarea | ~3 lines |
---
## 12. Select/Dropdown (Native)
**Penguin UI: `select/` (7 variants)**
| # | Location | What it is | Size |
|---|----------|------------|------|
| 29 | `assets/views/admin/catalog/product_form.html:53-60` | Category select | ~8 lines |
| 30 | `assets/views/admin/catalog/category_form.html:40-49` | Parent category select (tree indented with `—&nbsp;`) | ~10 lines |
| 31 | `assets/views/admin/orders/show.html:108-112` | Order status select | ~5 lines |
---
## 13. File Input
**Penguin UI: `file-input/`**
| # | Location | What it is | Size |
|---|----------|------------|------|
| 32 | `assets/views/admin/catalog/product_form.html:78-82` | Product image upload | ~5 lines |
| 33 | `assets/views/admin/catalog/category_form.html:58-64` | Category image upload | ~7 lines |
**Both use the same Tailwind `file:mr-3 file:...` prefix pattern for styling.**
---
## 14. Table
**Penguin UI: `table/` (7 variants)**
| # | Location | What it is | Size |
|---|----------|------------|------|
| 34 | `assets/views/admin/orders/index.html:11-36` | Orders table: number, customer, status pill, total, "View" link | ~26 lines |
| 35 | `assets/views/admin/orders/show.html:20-44` | Order items table: product, quantity, line total + tfoot summary | ~25 lines |
| 36 | `assets/views/admin/catalog/products.html:20-70` | Products table: image+name+category, price, stock, status pill, edit/view/delete actions | ~51 lines |
| 37 | `assets/views/admin/catalog/categories.html:20-59` | Categories table: tree-indented name, product count, status pill, edit/delete | ~40 lines |
| 38 | `assets/views/shop/_cart_body.html:6-59` | Cart table: product link, price, quantity input, line total, remove button + tfoot total | ~54 lines |
**Pattern:** Every table uses the same class structure:
```
<table class="w-full text-left text-sm">
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70">
<tbody class="divide-y divide-outline">
<tr class="hover:bg-surface-alt">
```
This is copy-pasted 5 times.
--- ---
## 15. Alert / Error Banner — ✅ DONE ## 15. Alert / Error Banner — ✅ DONE
**Penguin UI: `alert/default-alert.html`** **Penguin UI: `alert/default-alert.html`**
- Exact upstream mirror at `assets/views/penguinui/alert/default-alert.html` (reference only) - Exact upstream mirror at `penguinui-components/alert/default-alert.html` (reference only)
- Adapted into the `ui::alert_danger(message, extra="")` macro in - Adapted into the `ui::alert_danger(message, extra="")` macro in
`assets/views/macros/ui.html` (compact one-line danger alert + danger icon). `assets/views/macros/ui.html` (compact one-line danger alert + danger icon).
- Adopted at both sites: `admin/login.html` (login error) and - Adopted at both sites: `admin/login.html` (login error) and
@@ -273,7 +298,7 @@ This is copy-pasted 5 times.
## 16. Badge / Status Pill — ✅ DONE ## 16. Badge / Status Pill — ✅ DONE
**Penguin UI: `badge/soft-color-badge.html`** **Penguin UI: `badge/soft-color-badge.html`**
- Exact upstream mirror at `assets/views/penguinui/badge/soft-color-badge.html` (reference only) - Exact upstream mirror at `penguinui-components/badge/soft-color-badge.html` (reference only)
- Adapted into the `ui::badge(label, variant)` macro in `assets/views/macros/ui.html` - Adapted into the `ui::badge(label, variant)` macro in `assets/views/macros/ui.html`
(variants: success | danger | warning | info | primary | neutral). (variants: success | danger | warning | info | primary | neutral).
- Adopted at the status-pill sites: "Auth" badge (`admin/login.html`), order status - Adopted at the status-pill sites: "Auth" badge (`admin/login.html`), order status
@@ -285,25 +310,44 @@ This is copy-pasted 5 times.
--- ---
## 17. Buttons — ✅ DONE (canonical macros + adopted) ## 17. Buttons — ✅ DONE
**Penguin UI: `buttons/default-button.html`, `outline-button.html`, `ghost-button.html`, `button-with-icon.html`** **Penguin UI: `buttons/default-button.html`, `outline-button.html`, `ghost-button.html`, `button-with-icon.html`**
- Exact upstream mirrors at `assets/views/penguinui/buttons/*.html` (reference only). - Exact upstream mirrors at `penguinui-components/buttons/*.html` (reference only).
- Canonical button macros in `assets/views/macros/ui.html`: - Macros in `assets/views/macros/ui.html`:
`ui::button_primary`, `ui::button_outline` (neutral secondary), `ui::button_danger`, `ui::button(label, variant="primary", type, href, attrs, extra, icon, size="px-4 py-2 text-sm")`
`ui::button_ghost`. Each takes `label`, `type`, `href` (renders `<a>` vs `<button>`), and `ui::icon_button(icon, variant="ghost-secondary", aria_label, attrs, …)`.
`attrs` (raw — htmx / `:disabled` / name / value), `extra` (extra classes). The per-variant class strings are the **verbatim** Penguin variants (solid
Upstream's color-button token typos (`text-onDanger` etc.) are fixed to our real `primary|secondary|danger|success|warning|info`, `outline-*`, `ghost-*`) — only
tokens (`text-on-danger`). `inline-flex items-center justify-center gap-2` is added so `<a>`/`w-full`/`icon`
render, and upstream's `text-onDanger`/`text-onSuccess`… token typos are fixed to
our real `text-on-*` tokens. `href``<a>` else `<button>`; `attrs` is raw
(htmx / `:disabled` / name / value); `icon` is a raw `<svg>` rendered before the
label (Penguin button-with-icon).
- **Sizes are NOT normalized**: `size` defaults to Penguin's `px-4 py-2 text-sm`
but each call site that differed keeps it (`px-3 py-2` form-header cancels &
order back, `px-5 py-2` add-to-cart / cart-checkout / order-confirmed continue,
`px-6 py-2.5` checkout place-order, `px-3 py-1.5 text-xs` table actions).
- Adopted across every standard filled/outline/submit button: login, product & - Adopted across every standard filled/outline/submit button: login, product &
category forms (save/cancel), products/categories "new" + empty-state CTAs, category forms (save / cancel = `outline-secondary`), products/categories "new" +
orders detail (back/ship/status), shipping save, cart (continue/checkout/empty), empty-state CTAs, orders detail (back/ship/status), shipping save, cart
checkout place-order (`:disabled` via `attrs`), product detail add-to-cart, (continue/checkout/empty), checkout place-order (`:disabled` via `attrs`),
order-confirmed continue. product detail add-to-cart, order-confirmed continue.
- Intentionally left inline (different components, not the standard button): - Icon-only buttons now use `ui::icon_button(icon, variant="ghost-secondary",
icon-only ghost buttons (#51 gear/hamburger/chevron), compact table row-action aria_label, attrs, …)` — Penguin ghost treatment, square. Converted: settings
buttons (`edit`/`view`/`delete`, `px-3 py-1.5 text-xs`), the `text-danger` gear, both hamburgers (site + admin), admin sidebar toggle, mobile category
"Remove" text link (#52), the file-input button (#13), and nav menu links. toggle. The cart link (live `x-init` badge) and the category-accordion chevron
keep the same Penguin ghost classes **inline** only because their markup mixes
single+double quotes that can't be passed through a Tera macro arg — visually
identical to `icon_button`.
- Table row-actions (`edit`/`view`/`delete`/`View`/`label`) → `ui::button`
`outline-secondary` / `outline-danger` at `size="px-3 py-1.5 text-xs"`; cart
"Remove" → `ghost-danger`; card add-to-cart → `ui::button` with the cart `icon`.
- Still genuinely not this component (tracked elsewhere): toast dismiss/Reply
buttons (part of the vendored toast mirror, already Penguin), settings dropdown
menu items (Penguin dropdown items), gallery thumbnail buttons (carousel),
sidebar logout/exit (Penguin sidebar link treatment), and navbar nav-menu
links/logout (belong to §1 Navbar). The file-input button is §13.
> Gotcha for future macro use: Tera renders `{% include %}` in the **includer's** > Gotcha for future macro use: Tera renders `{% include %}` in the **includer's**
> macro scope, so a template that includes a partial which calls `ui::` must also > macro scope, so a template that includes a partial which calls `ui::` must also
@@ -313,8 +357,14 @@ This is copy-pasted 5 times.
--- ---
## 18. Toggle / Switch ## 18. Toggle / Switch — LOW PRIORITY (de-duplication, not replacement)
**Penguin UI: `toggle/` (2 variants)** **Penguin UI: `toggle/` (3 variants)**
> **Priority: LOW** | ~36 lines,100% duplicated between `base.html` and `admin/base.html`.
> This is JavaScript theme-switching logic (`applyTheme`, `setTheme`, `matchMedia`),
> not a CSS toggle component. Penguin's `toggle/default-toggle.html` is a visual
> on/off switch — not applicable here.
> **Action:** de-duplicate the JS into a shared partial rather than porting.
| # | Location | What it is | Size | | # | Location | What it is | Size |
|---|----------|------------|------| |---|----------|------------|------|
@@ -328,26 +378,36 @@ This is copy-pasted 5 times.
--- ---
## 19. Inline SVG Icons ## 19. Inline SVG Icons — MOSTLY DONE
**Penguin UI: none (Penguin uses Heroicons-equivalent inline SVGs)** **Penguin UI: none (Penguin uses Heroicons-equivalent inline SVGs)**
| # | Location | Icon | Occurrences | > **Priority: LOW.** Penguin ships no icon library, so this is dedup, not a port.
|---|----------|------|-------------| > The repeated hamburger / close / cart SVGs are now centralized in the
| 55 | `base.html:70-72,168-170` | Hamburger (3-line menu) | 2 | > `ui::icon(name, size, extra, attrs)` macro (`macros/ui.html`); call sites use
| 56 | `base.html:104-105` | Shopping cart | 1 | > `{{ ui::icon(name="cart") }}` etc. The chevron dropdown arrows stay inline by
| 57 | `base.html:116-121` | Gear/cog (settings) | 1 | > design — they carry nested-quote Alpine `:class` / `x-bind:class` bindings,
| 58 | ~~`base.html:220-221`~~ | Checkmark (toast success) | ✅ removed — now in vendored toast component | > which Tera macro args can't pass cleanly (see the attrs note atop `ui.html`).
| 59 | `checkout.html:62-64,115-117` | Chevron-down (dropdown arrow) | 2 |
| 60 | `_sidebar.html:30-33` | Chevron-right (accordion expand) | 1 |
| 61 | `admin/base.html:106-108` | Hamburger (admin sidebar toggle) | 1 |
| 62 | `admin/base.html:121-125` | Gear/cog (admin settings) | 1 |
All are raw inline `<svg>` with hardcoded `<path d="...">` — no icon library, no partials. | # | Location | Icon | Status |
|---|----------|------|--------|
| 55 | `base.html` (categories + mobile toggle), `admin/base.html` (sidebar toggle) | Hamburger (3-line menu) | ✅ `ui::icon(name="hamburger")` |
| 56 | `base.html` (mobile toggle), `admin/base.html` (sidebar toggle) | Close (X) | ✅ `ui::icon(name="close")` |
| 57 | `base.html` (cart link) | Shopping cart | ✅ `ui::icon(name="cart")` |
| 58 | ~~`base.html:220-221`~~ | Checkmark (toast success) | ✅ removed — now in vendored toast component |
| 59 | `checkout.html:61,110` | Chevron-down (dropdown arrow) | inline — nested-quote `:class` binding |
| 60 | `_sidebar.html:35` | Chevron-down (accordion expand, rotates) | inline — nested-quote `x-bind:class` binding |
| 61 | `settings_dropdown.html` | Gear/cog (settings) | inline in `ui::icon_button` call (shared partial; single use) |
Remaining inline `<svg>` are the rotating chevrons (kept inline on purpose, above).
--- ---
## 20. Empty State ## 20. Empty State — LOW PRIORITY
**Penguin UI: no direct component, but table empty states exist in Penguin tables** **Penguin UI: no dedicated empty-state component**
> **Priority: LOW** | ~22 lines across5 sites.
> These are simple `<div>` messages, often with a CTA button already using
> `ui::button`. Nothing to port — already consistent with project styling.
| # | Location | What it is | Size | | # | Location | What it is | Size |
|---|----------|------------|------| |---|----------|------------|------|
@@ -359,8 +419,13 @@ All are raw inline `<svg>` with hardcoded `<path d="...">` — no icon library,
--- ---
## 21. Dashboard Navigation Cards ## 21. Dashboard Navigation Cards — LOW PRIORITY
**Penguin UI: `card/`** **Penguin UI: `card/default-card.html` or `card/card-with-button.html`**
> **Priority: LOW** | ~16 lines.
> Already uses card styling (`rounded-radius border border-outline hover:border-primary`).
> Penguin's `default-card.html` adds a structured header/body layout — adopt if
> cards ever grow beyond a title+description link.
| # | Location | What it is | Size | | # | Location | What it is | Size |
|---|----------|------------|------| |---|----------|------------|------|
@@ -372,8 +437,13 @@ All are raw inline `<svg>` with hardcoded `<path d="...">` — no icon library,
--- ---
## 22. Checkout Order Summary ## 22. Checkout Order Summary — LOW PRIORITY
**Penguin UI: `card/` (ecommerce-summary style)** **Penguin UI: `card/`**
> **Priority: LOW** | ~29 lines.
> Already uses card-like styling with `tabular-nums`. All internal buttons use
> `ui::button`. The only handcoded part is the outer `<div>` wrapper and line-item
> layout. Penguin doesn't have an ecommerce-specific summary component.
| # | Location | What it is | Size | | # | Location | What it is | Size |
|---|----------|------------|------| |---|----------|------------|------|
@@ -387,8 +457,14 @@ All are raw inline `<svg>` with hardcoded `<path d="...">` — no icon library,
--- ---
## 23. Login Card ## 23. Login Card — LOW PRIORITY
**Penguin UI: `card/`** **Penguin UI: `card/default-card.html`**
> **Priority: LOW** | ~56 lines.
> Already fully uses Penguin macros inside: `ui::input`, `ui::button`,
> `ui::badge`, `ui::alert_danger`. Only the outer card wrapper (border,
> bg-surface-alt, shadow-sm) is handcoded. Adopting `default-card.html`
> would add visual polish but little functional gain.
| # | Location | What it is | Size | | # | Location | What it is | Size |
|---|----------|------------|------| |---|----------|------------|------|
@@ -396,9 +472,15 @@ All are raw inline `<svg>` with hardcoded `<path d="...">` — no icon library,
--- ---
## 24. Checkout Fieldset Cards ## 24. Checkout Fieldset Cards — LOW PRIORITY
**Penguin UI: `card/`** **Penguin UI: `card/`**
> **Priority: LOW** | ~142 lines across4 fieldsets.
> Already uses card styling (`rounded-radius border border-outline bg-surface p-6`)
> and all internal form controls use Penguin macros (`ui::input`, `ui::textarea`,
> `ui::button`). Only the `<fieldset>` + `<legend>` wrapping is handcoded.
> Low value in replacing — fieldset semantics are correct here.
| # | Location | What it is | Size | | # | Location | What it is | Size |
|---|----------|------------|------| |---|----------|------------|------|
| 71 | `assets/views/shop/checkout.html:34-79` | Contact info fieldset (email, name, phone+prefix) | ~46 lines | | 71 | `assets/views/shop/checkout.html:34-79` | Contact info fieldset (email, name, phone+prefix) | ~46 lines |
@@ -410,9 +492,14 @@ Each fieldset uses `<fieldset>` + `<legend>` with the same `rounded-radius borde
--- ---
## 25. Order Detail Info Panel ## 25. Order Detail Info Panel — LOW PRIORITY
**Penguin UI: `card/`** **Penguin UI: `card/`**
> **Priority: LOW** | ~64 lines across3 panels.
> Already card-styled (`rounded-radius border border-outline bg-surface p-5`)
> with Penguin macros inside. Deviating from this simple structure would
> make the dense info layout harder to scan.
| # | Location | What it is | Size | | # | Location | What it is | Size |
|---|----------|------------|------| |---|----------|------------|------|
| 75 | `assets/views/admin/orders/show.html:49-77` | Customer + shipping + payment info panel | ~29 lines | | 75 | `assets/views/admin/orders/show.html:49-77` | Customer + shipping + payment info panel | ~29 lines |
@@ -421,18 +508,27 @@ Each fieldset uses `<fieldset>` + `<legend>` with the same `rounded-radius borde
--- ---
## 26. Shipping Method Settings Row ## 26. Shipping Method Settings Row — LOW PRIORITY
**Penguin UI: `card/`** **Penguin UI: `card/`**
> **Priority: LOW** | ~21 lines.
> Already fully uses Penguin macros: `ui::input`, `ui::checkbox`, `ui::button`.
> The card wrapper is the same pattern as other admin panels. Nothing to port.
| # | Location | What it is | Size | | # | Location | What it is | Size |
|---|----------|------------|------| |---|----------|------------|------|
| 78 | `assets/views/admin/shipping/index.html:14-34` | Per-carrier settings: name label, price input, enabled checkbox, save button | ~21 lines | | 78 | `assets/views/admin/shipping/index.html:14-34` | Per-carrier settings: name label, price input, enabled checkbox, save button | ~21 lines |
--- ---
## 27. Product/Category Form Wrapper ## 27. Product/Category Form Wrapper — LOW PRIORITY
**Penguin UI: `card/`** **Penguin UI: `card/`**
> **Priority: LOW** | ~150 lines across2 forms.
> Already fully uses Penguin macros: `ui::input`, `ui::textarea`, `ui::select`,
> `ui::file_input`, `ui::checkbox`, `ui::button`. The `<form>` card wrapper is
> the same border/bg pattern. Penguin doesn't have a form-specific layout component.
| # | Location | What it is | Size | | # | Location | What it is | Size |
|---|----------|------------|------| |---|----------|------------|------|
| 79 | `assets/views/admin/catalog/product_form.html:15-99` | Full product edit/create form with all fields | ~84 lines | | 79 | `assets/views/admin/catalog/product_form.html:15-99` | Full product edit/create form with all fields | ~84 lines |
@@ -442,43 +538,100 @@ Both are wrapped in a single card-style `<form>`.
--- ---
## Porting Roadmap (priority order)
### Phase 1 — HIGH (direct Penguin matches, clear win)
| # | Component | Penguin match | Est. effort | Lines saved |
|---|-----------|---------------|-------------|-------------|
| ~~5~~ | ~~Country/Phone Combobox~~ | ⛔ WON'T PORT — needs Alpine Focus plugin; our lightweight version kept | — | — |
| ~~7~~ | ~~Image Gallery~~ | ✅ DONE — `carousel/default-carousel.html` | Small | ~19 |
| ~~8~~ | ~~Radio Groups~~ | ✅ DONE — `radio/radio-with-container.html` | Small | ~47 |
| ~~14~~ | ~~Table~~ | ✅ DONE — `table/default-table.html` (class-string macros) | Medium | ~196 |
### Phase 2 — MEDIUM (good match, more integration risk)
| # | Component | Penguin match | Est. effort | Lines saved |
|---|-----------|---------------|-------------|-------------|
| ~~1~~ | ~~Navbar~~ | ✅ DONE — `navbar/default-navbar.html` (link treatment + animated hamburger; `ui::nav_link`) | Large | ~143 |
### Phase 3 — LOW (mostly already Penguin, or no good match)
| # | Component | Action |
|---|-----------|--------|
| 18 | Toggle/Switch | De-duplicate JS into shared partial (not a Penguin port) |
| 19 | Inline SVG Icons | Optional: extract `ui::icon(name)` macro |
| 20 | Empty State | Already fine — nothing to port |
| 21 | Dashboard Cards | Adopt `card/default-card.html` if cards grow |
| 22 | Checkout Summary | Already fine — nothing to port |
| 23 | Login Card | Already fine — only outer wrapper is handcoded |
| 24 | Checkout Fieldsets | Already fine — only `<fieldset>` wrapper is handcoded |
| 25 | Order Info Panels | Already fine — only card wrappers are handcoded |
| 26 | Shipping Settings Row | Already fully uses Penguin macros |
| 27 | Form Wrappers | Already fully uses Penguin macros |
### Already DONE (17 of 27)
| # | Component |
|---|-----------|
| 0 | Toast |
| 1 | Navbar |
| 2 | Sidebar (Admin) |
| 3 | Sidebar (Category Accordion) |
| 4 | Dropdown (Settings) |
| 6 | Product Card |
| 7 | Image Gallery |
| 8 | Radio Groups |
| 9 | Checkbox |
| 10 | Text Input |
| 11 | Textarea |
| 12 | Select/Dropdown |
| 13 | File Input |
| 14 | Table |
| 15 | Alert / Error Banner |
| 16 | Badge / Status Pill |
| 17 | Buttons |
**No real ports remain.** #5 Combobox is a conscious WON'T-PORT (Alpine Focus
plugin dependency). All Phase-3 items (#1827) are already internally
Penguin-adapted or have no applicable component — leave as-is.
---
## Summary ## Summary
| # | Component | Penguin UI Directory | Handcoded Instances | Total Lines | | # | Component | Penguin UI Directory | Status | Lines |
|---|-----------|---------------------|--------------------|-------------| |---|-----------|---------------------|--------|-------|
| 1 | Navbar | `navbar/` | 2 | ~143 | | 0 | Toast | `toast-notification/` | ✅ DONE | — |
| 2 | Sidebar (admin) | `sidebar/` | 2 | ~46 | | 1 | Navbar | `navbar/` | ✅ DONE | ~143 |
| 3 | Sidebar (category accordion) | `sidebar/` | 2 | ~62 | | 2 | Sidebar (admin) | `sidebar/` | ✅ DONE | ~46 |
| 4 | Dropdown (settings) | `dropdown-menu/` | **2 duplicates** | ~103 | | 3 | Sidebar (category accordion) | `sidebar/` | ✅ DONE | ~62 |
| 5 | Country/Phone combobox | `text-input/` | 2 | ~51 | | 4 | Dropdown (settings) | `dropdowns/` | ✅ DONE | ~103 |
| 6 | Product card | `card/` | 1 | ~30 | | 5 | Country/Phone combobox | `combobox/` | ⛔ WON'T PORT | ~51 |
| 7 | Image gallery | `carousel/` | 1 | ~19 | | 6 | Product card | `card/` | ✅ DONE | ~30 |
| 8 | Radio groups | (form inputs) | 2 | ~47 | | 7 | Image gallery | `carousel/` | ✅ DONE | ~19 |
| 9 | Checkbox | `checkbox/` | 3 | ~15 | | 8 | Radio groups | `radio/` | ✅ DONE | ~47 |
| 10 | Text input | `text-input/` | 8 | ~146 | | 9 | Checkbox | `checkbox/` | ✅ DONE | ~15 |
| 11 | Textarea | `textarea/` | 3 | ~10 | | 10 | Text input | `text-input/` | ✅ DONE | ~146 |
| 12 | Select | `select/` | 3 | ~23 | | 11 | Textarea | `text-area/` | ✅ DONE | ~10 |
| 13 | File input | `file-input/` | 2 | ~12 | | 12 | Select | `select/` | ✅ DONE | ~23 |
| 14 | Table | `table/` | 5 | ~196 | | 13 | File input | `file-input/` | ✅ DONE | ~12 |
| 15 | Alert/Error | `alert/` | 2 | ~9 | | 14 | Table | `table/` | ✅ DONE | ~196 |
| 16 | Badge/Pill | `badge/` | 6 | ~17 | | 15 | Alert/Error | `alert/` | ✅ DONE | ~9 |
| 17 | Button | `buttons/` | 50+ occurrences | ~200+ | | 16 | Badge/Pill | `badge/` | ✅ DONE | ~17 |
| 18 | Toggle (theme) | `toggle/` | **2 duplicates** | ~36 | | 17 | Button | `buttons/` | ✅ DONE | ~200+ |
| 19 | Inline SVG icons | N/A | 8 distinct icons | ~50 | | 18 | Toggle (theme) | `toggle/` | LOW (dedup) | ~36 |
| 20 | Empty state | (table variants) | 5 | ~22 | | 19 | Inline SVG icons | N/A | LOW | ~50 |
| 21 | Dashboard cards | `card/` | 1 | ~16 | | 20 | Empty state | N/A | LOW | ~22 |
| 22 | Checkout summary | `card/` | 1 | ~29 | | 21 | Dashboard cards | `card/` | LOW | ~16 |
| 23 | Login card | `card/` | 1 | ~56 | | 22 | Checkout summary | `card/` | LOW | ~29 |
| 24 | Checkout fieldsets | `card/` | 4 | ~142 | | 23 | Login card | `card/` | LOW | ~56 |
| 25 | Order info panels | `card/` | 3 | ~64 | | 24 | Checkout fieldsets | `card/` | LOW | ~142 |
| 26 | Shipping settings row | `card/` | 1 | ~21 | | 25 | Order info panels | `card/` | LOW | ~64 |
| 27 | Form wrappers | `card/` | 2 | ~150 | | 26 | Shipping settings row | `card/` | LOW | ~21 |
| 27 | Form wrappers | `card/` | LOW | ~150 |
**Grand total: ~27 distinct handcoded UI component types across ~80 instances, representing approximately 1,600+ lines of handcoded HTML/Tailwind/Alpine that could be replaced by Penguin UI components.** **Status: 17 of 27 components fully ported to Penguin UI. No real ports remain.
#5 Combobox is a conscious WON'T-PORT (Alpine Focus plugin dependency). The
### Duplication hotspots: remaining Phase-3 items (#1827) are already internally Penguin-adapted or have
- **Settings dropdown** (`base.html:110-162` and `admin/base.html:117-166`) — 100% copy-paste no applicable match — the migration is effectively complete.**
- **Theme toggle JS** (`base.html:13-30` and `admin/base.html:13-30`) — 100% copy-paste
- **Text input class string** — same 80-character Tailwind string appears 15+ times
- **Table class strings** (thead, tbody, tr) — copy-pasted 5 times
- **Button variants** — inconsistent `hover:opacity-75` vs `hover:opacity-90`

View File

@@ -13,7 +13,7 @@ loco-rs = { workspace = true }
[dependencies.sea-orm-migration] [dependencies.sea-orm-migration]
version = "1.1.0" version = "1.1.20"
features = [ features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.

View File

@@ -30,6 +30,10 @@ mod m20260616_160000_add_parent_to_categories;
mod m20260617_000001_add_carrier_to_shipping_methods; mod m20260617_000001_add_carrier_to_shipping_methods;
mod m20260617_000002_add_shipment_to_orders; mod m20260617_000002_add_shipment_to_orders;
mod m20260617_000003_add_phone_to_orders; mod m20260617_000003_add_phone_to_orders;
mod m20260618_000001_o_auth2_sessions;
mod m20260618_000002_customer_profiles;
mod m20260618_000003_account_type;
mod m20260618_000004_account_ownership;
pub struct Migrator; pub struct Migrator;
#[async_trait::async_trait] #[async_trait::async_trait]
@@ -64,6 +68,10 @@ impl MigratorTrait for Migrator {
Box::new(m20260617_000001_add_carrier_to_shipping_methods::Migration), Box::new(m20260617_000001_add_carrier_to_shipping_methods::Migration),
Box::new(m20260617_000002_add_shipment_to_orders::Migration), Box::new(m20260617_000002_add_shipment_to_orders::Migration),
Box::new(m20260617_000003_add_phone_to_orders::Migration), Box::new(m20260617_000003_add_phone_to_orders::Migration),
Box::new(m20260618_000001_o_auth2_sessions::Migration),
Box::new(m20260618_000002_customer_profiles::Migration),
Box::new(m20260618_000003_account_type::Migration),
Box::new(m20260618_000004_account_ownership::Migration),
// inject-above (do not remove this comment) // inject-above (do not remove this comment)
] ]
} }

View File

@@ -0,0 +1,29 @@
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
// OAuth2 session store used by loco-oauth2 to correlate the provider's
// access token with a local user during the callback flow. `user` adds
// a user_id FK to the users table.
create_table(
m,
"o_auth2_sessions",
&[
("id", ColType::PkAuto),
("session_id", ColType::StringUniq),
("expires_at", ColType::TimestampWithTimeZone),
],
&[("user", "")],
)
.await
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
drop_table(m, "o_auth2_sessions").await
}
}

View File

@@ -0,0 +1,44 @@
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
// One shipping/contact profile per customer, used to prefill the
// checkout form. `name`/`email` already live on `users`; this table
// holds only the address + phone fields. `user` adds a user_id FK; the
// unique index below makes the relationship 1:1.
create_table(
m,
"customer_profiles",
&[
("id", ColType::PkAuto),
("phone_prefix", ColType::StringNull),
("phone", ColType::StringNull),
("address", ColType::StringNull),
("city", ColType::StringNull),
("zip", ColType::StringNull),
("country", ColType::StringNull),
],
&[("user", "")],
)
.await?;
m.create_index(
Index::create()
.name("idx_customer_profiles_user_id_unique")
.table(Alias::new("customer_profiles"))
.col(Alias::new("user_id"))
.unique()
.to_owned(),
)
.await
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
drop_table(m, "customer_profiles").await
}
}

View File

@@ -0,0 +1,39 @@
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
// Personal vs company purchasing. `account_type` is "personal" or "company";
// the company_* columns hold the Slovak invoicing identifiers (IČO, DIČ and the
// optional VAT id IČ DPH) and are only filled for company accounts/orders.
const COMPANY_COLUMNS: [&str; 4] = ["company_name", "company_id", "tax_id", "vat_id"];
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
for table in ["customer_profiles", "orders"] {
add_column(
m,
table,
"account_type",
ColType::StringWithDefault("personal".to_string()),
)
.await?;
for col in COMPANY_COLUMNS {
add_column(m, table, col, ColType::StringNull).await?;
}
}
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
for table in ["customer_profiles", "orders"] {
remove_column(m, table, "account_type").await?;
for col in COMPANY_COLUMNS {
remove_column(m, table, col).await?;
}
}
Ok(())
}
}

View File

@@ -0,0 +1,38 @@
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
// Account type becomes a permanent property of the *user* (chosen at
// registration, never switchable), so it moves off `customer_profiles`. Orders
// gain a nullable `user_id` linking them to the account that placed them
// (null for guest orders that didn't create an account).
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
add_column(
m,
"users",
"account_type",
ColType::StringWithDefault("personal".to_string()),
)
.await?;
add_column(m, "orders", "user_id", ColType::IntegerNull).await?;
remove_column(m, "customer_profiles", "account_type").await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
add_column(
m,
"customer_profiles",
"account_type",
ColType::StringWithDefault("personal".to_string()),
)
.await?;
remove_column(m, "orders", "user_id").await?;
remove_column(m, "users", "account_type").await?;
Ok(())
}
}

View File

@@ -0,0 +1,30 @@
# 🐧 Penguin UI
<div align="center">
![Penguin UI](https://img.shields.io/badge/Penguin%20UI-v4.0-blue?style=for-the-badge&logo=tailwindcss)
![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-4.0-38B2AC?style=for-the-badge&logo=tailwindcss)
![Alpine.js](https://img.shields.io/badge/Alpine.js-3.0-77C1D2?style=for-the-badge&logo=alpine.js)
*A collection of beautifully designed UI components built with **Tailwind CSS** and **Alpine.js***
[📚 **Documentation**](https://www.penguinui.com) • [🚀 **Getting Started**](https://www.penguinui.com/docs/getting-started)
</div>
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](https://www.penguinui.com/docs/license) for details.
---
<div align="center">
**Created by Salar Houshvand**
[![X](https://img.shields.io/badge/salar_houshvand-000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/salar_houshvand)
</div>

View File

@@ -0,0 +1,69 @@
<div class="w-full divide-y divide-outline text-on-surface dark:divide-outline-dark dark:text-on-surface-dark">
<div x-data="{ isExpanded: false }">
<button id="controlsAccordionItemOne" type="button"
class="flex w-full items-center justify-between gap-4 py-4 text-left underline-offset-2 focus-visible:underline focus-visible:outline-hidden"
aria-controls="accordionItemOne" x-on:click="isExpanded = ! isExpanded"
x-bind:class="isExpanded ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="isExpanded ? 'true' : 'false'">
What browsers are supported?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="isExpanded ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="isExpanded" id="accordionItemOne" role="region" aria-labelledby="controlsAccordionItemOne"
x-collapse>
<div class="pb-4 text-sm sm:text-base text-pretty">
Our website is optimized for the latest versions of Chrome, Firefox, Safari, and Edge. Check our <a
href="#" class="underline underline-offset-2 text-primary dark:text-primary-dark">documentation</a>
for additional information.
</div>
</div>
</div>
<div x-data="{ isExpanded: false }">
<button id="controlsAccordionItemTwo" type="button"
class="flex w-full items-center justify-between gap-4 py-4 text-left underline-offset-2 focus-visible:underline focus-visible:outline-hidden"
aria-controls="accordionItemTwo" x-on:click="isExpanded = ! isExpanded"
x-bind:class="isExpanded ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="isExpanded ? 'true' : 'false'">
How can I contact customer support?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="isExpanded ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="isExpanded" id="accordionItemTwo" role="region" aria-labelledby="controlsAccordionItemTwo"
x-collapse>
<div class="pb-4 text-sm sm:text-base text-pretty">
Reach out to our dedicated support team via email at <a href="#"
class="underline underline-offset-2 text-primary dark:text-primary-dark">support@example.com</a> or
call our toll-free number at 1-800-123-4567 during business hours.
</div>
</div>
</div>
<div x-data="{ isExpanded: false }">
<button id="controlsAccordionItemThree" type="button"
class="flex w-full items-center justify-between gap-4 py-4 text-left underline-offset-2 focus-visible:underline focus-visible:outline-hidden"
aria-controls="accordionItemThree" x-on:click="isExpanded = ! isExpanded"
x-bind:class="isExpanded ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="isExpanded ? 'true' : 'false'">
What is the refund policy?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="isExpanded ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="isExpanded" id="accordionItemThree" role="region"
aria-labelledby="controlsAccordionItemThree" x-collapse>
<div class="pb-4 text-sm sm:text-base text-pretty">
Please refer to our <a href="#"
class="underline underline-offset-2 text-primary dark:text-primary-dark">refund policy page</a> on
the website for detailed information regarding eligibility, timeframes, and the process for requesting a
refund.
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,70 @@
<div
class="w-full divide-y divide-outline overflow-hidden rounded-radius border border-outline bg-surface-alt/40 text-on-surface dark:divide-outline-dark dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark">
<div x-data="{ isExpanded: false }">
<button id="controlsAccordionItemOne" type="button"
class="flex w-full items-center justify-between gap-4 bg-surface-alt p-4 text-left underline-offset-2 hover:bg-surface-alt/75 focus-visible:bg-surface-alt/75 focus-visible:underline focus-visible:outline-hidden dark:bg-surface-dark-alt dark:hover:bg-surface-dark-alt/75 dark:focus-visible:bg-surface-dark-alt/75"
aria-controls="accordionItemOne" x-on:click="isExpanded = ! isExpanded"
x-bind:class="isExpanded ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="isExpanded ? 'true' : 'false'">
What browsers are supported?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="isExpanded ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="isExpanded" id="accordionItemOne" role="region" aria-labelledby="controlsAccordionItemOne"
x-collapse>
<div class="p-4 text-sm sm:text-base text-pretty">
Our website is optimized for the latest versions of Chrome, Firefox, Safari, and Edge. Check our <a
href="#" class="underline underline-offset-2 text-primary dark:text-primary-dark">documentation</a>
for additional information.
</div>
</div>
</div>
<div x-data="{ isExpanded: false }">
<button id="controlsAccordionItemTwo" type="button"
class="flex w-full items-center justify-between gap-4 bg-surface-alt p-4 text-left underline-offset-2 hover:bg-surface-alt/75 focus-visible:bg-surface-alt/75 focus-visible:underline focus-visible:outline-hidden dark:bg-surface-dark-alt dark:hover:bg-surface-dark-alt/75 dark:focus-visible:bg-surface-dark-alt/75"
aria-controls="accordionItemTwo" x-on:click="isExpanded = ! isExpanded"
x-bind:class="isExpanded ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="isExpanded ? 'true' : 'false'">
How can I contact customer support?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="isExpanded ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="isExpanded" id="accordionItemTwo" role="region" aria-labelledby="controlsAccordionItemTwo"
x-collapse>
<div class="p-4 text-sm sm:text-base text-pretty">
Reach out to our dedicated support team via email at <a href="#"
class="underline underline-offset-2 text-primary dark:text-primary-dark">support@example.com</a> or
call our toll-free number at 1-800-123-4567 during business hours.
</div>
</div>
</div>
<div x-data="{ isExpanded: false }">
<button id="controlsAccordionItemThree" type="button"
class="flex w-full items-center justify-between gap-4 bg-surface-alt p-4 text-left underline-offset-2 hover:bg-surface-alt/75 focus-visible:bg-surface-alt/75 focus-visible:underline focus-visible:outline-hidden dark:bg-surface-dark-alt dark:hover:bg-surface-dark-alt/75 dark:focus-visible:bg-surface-dark-alt/75"
aria-controls="accordionItemThree" x-on:click="isExpanded = ! isExpanded"
x-bind:class="isExpanded ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="isExpanded ? 'true' : 'false'">
What is the refund policy?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="isExpanded ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="isExpanded" id="accordionItemThree" role="region"
aria-labelledby="controlsAccordionItemThree" x-collapse>
<div class="p-4 text-sm sm:text-base text-pretty">
Please refer to our <a href="#"
class="underline underline-offset-2 text-primary dark:text-primary-dark">refund policy page</a> on
the website for detailed information regarding eligibility, timeframes, and the process for requesting a
refund.
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,70 @@
<div x-data="{ selectedAccordionItem: 'one' }"
class="w-full divide-y divide-outline overflow-hidden rounded-sm border border-outline bg-surface-alt/40 text-on-surface dark:divide-outline-dark dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark">
<div>
<button id="controlsAccordionItemOne" type="button"
class="flex w-full items-center justify-between gap-4 bg-surface-alt p-4 text-left underline-offset-2 hover:bg-surface-alt/75 focus-visible:bg-surface-alt/75 focus-visible:underline focus-visible:outline-hidden dark:bg-surface-dark-alt dark:hover:bg-surface-dark-alt/75 dark:focus-visible:bg-surface-dark-alt/75"
aria-controls="accordionItemOne" x-on:click="selectedAccordionItem = 'one'"
x-bind:class="selectedAccordionItem === 'one' ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="selectedAccordionItem === 'one' ? 'true' : 'false'">
What browsers are supported?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="selectedAccordionItem === 'one' ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="selectedAccordionItem === 'one'" id="accordionItemOne" role="region"
aria-labelledby="controlsAccordionItemOne" x-collapse>
<div class="p-4 text-sm sm:text-base text-pretty">
Our website is optimized for the latest versions of Chrome, Firefox, Safari, and Edge. Check our <a
href="#" class="underline underline-offset-2 text-primary dark:text-primary-dark">documentation</a>
for additional information.
</div>
</div>
</div>
<div>
<button id="controlsAccordionItemTwo" type="button"
class="flex w-full items-center justify-between gap-4 bg-surface-alt p-4 text-left underline-offset-2 hover:bg-surface-alt/75 focus-visible:bg-surface-alt/75 focus-visible:underline focus-visible:outline-hidden dark:bg-surface-dark-alt dark:hover:bg-surface-dark-alt/75 dark:focus-visible:bg-surface-dark-alt/75"
aria-controls="accordionItemTwo" x-on:click="selectedAccordionItem = 'two'"
x-bind:class="selectedAccordionItem === 'two' ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="selectedAccordionItem === 'two' ? 'true' : 'false'">
How can I contact customer support?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="selectedAccordionItem === 'two' ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="selectedAccordionItem === 'two'" id="accordionItemTwo" role="region"
aria-labelledby="controlsAccordionItemTwo" x-collapse>
<div class="p-4 text-sm sm:text-base text-pretty">
Reach out to our dedicated support team via email at <a href="#"
class="underline underline-offset-2 text-primary dark:text-primary-dark">support@example.com</a> or
call our toll-free number at 1-800-123-4567 during business hours.
</div>
</div>
</div>
<div>
<button id="controlsAccordionItemThree" type="button"
class="flex w-full items-center justify-between gap-4 bg-surface-alt p-4 text-left underline-offset-2 hover:bg-surface-alt/75 focus-visible:bg-surface-alt/75 focus-visible:underline focus-visible:outline-hidden dark:bg-surface-dark-alt dark:hover:bg-surface-dark-alt/75 dark:focus-visible:bg-surface-dark-alt/75"
aria-controls="accordionItemThree" x-on:click="selectedAccordionItem = 'three'"
x-bind:class="selectedAccordionItem === 'three' ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="selectedAccordionItem === 'three' ? 'true' : 'false'">
What is the refund policy?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="selectedAccordionItem === 'three' ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="selectedAccordionItem === 'three'" id="accordionItemThree" role="region"
aria-labelledby="controlsAccordionItemThree" x-collapse>
<div class="p-4 text-sm sm:text-base text-pretty">
Please refer to our <a href="#"
class="underline underline-offset-2 text-primary dark:text-primary-dark">refund policy page</a> on
the website for detailed information regarding eligibility, timeframes, and the process for requesting a
refund.
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,72 @@
<div class="flex w-full flex-col gap-4 text-on-surface dark:text-on-surface-dark">
<div x-data="{ isExpanded: false }"
class="overflow-hidden rounded-radius border border-outline bg-surface-alt/40 dark:border-outline-dark dark:bg-surface-dark-alt/50">
<button id="controlsAccordionItemOne" type="button"
class="flex w-full items-center justify-between gap-2 bg-surface-alt p-4 text-left underline-offset-2 hover:bg-surface-alt/75 focus-visible:bg-surface-alt/75 focus-visible:underline focus-visible:outline-hidden dark:bg-surface-dark-alt dark:hover:bg-surface-dark-alt/75 dark:focus-visible:bg-surface-dark-alt/75"
aria-controls="accordionItemOne" x-on:click="isExpanded = ! isExpanded"
x-bind:class="isExpanded ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="isExpanded ? 'true' : 'false'">
What browsers are supported?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="isExpanded ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="isExpanded" id="accordionItemOne" role="region" aria-labelledby="controlsAccordionItemOne"
x-collapse>
<div class="p-4 text-sm sm:text-base text-pretty">
Our website is optimized for the latest versions of Chrome, Firefox, Safari, and Edge. Check our <a
href="#" class="underline underline-offset-2 text-primary dark:text-primary-dark">documentation</a>
for additional information.
</div>
</div>
</div>
<div x-data="{ isExpanded: false }"
class="overflow-hidden rounded-radius border border-outline bg-surface-alt/40 dark:border-outline-dark dark:bg-surface-dark-alt/50">
<button id="controlsAccordionItemTwo" type="button"
class="flex w-full items-center justify-between gap-2 bg-surface-alt p-4 text-left underline-offset-2 hover:bg-surface-alt/75 focus-visible:bg-surface-alt/75 focus-visible:underline focus-visible:outline-hidden dark:bg-surface-dark-alt dark:hover:bg-surface-dark-alt/75 dark:focus-visible:bg-surface-dark-alt/75"
aria-controls="accordionItemTwo" x-on:click="isExpanded = ! isExpanded"
x-bind:class="isExpanded ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="isExpanded ? 'true' : 'false'">
How can I contact customer support?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="isExpanded ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="isExpanded" id="accordionItemTwo" role="region" aria-labelledby="controlsAccordionItemTwo"
x-collapse>
<div class="p-4 text-sm sm:text-base text-pretty">
Reach out to our dedicated support team via email at <a href="#"
class="underline underline-offset-2 text-primary dark:text-primary-dark">support@example.com</a> or
call our toll-free number at 1-800-123-4567 during business hours.
</div>
</div>
</div>
<div x-data="{ isExpanded: false }"
class="overflow-hidden rounded-radius border border-outline bg-surface-alt/40 dark:border-outline-dark dark:bg-surface-dark-alt/50">
<button id="controlsAccordionItemThree" type="button"
class="flex w-full items-center justify-between gap-2 bg-surface-alt p-4 text-left underline-offset-2 hover:bg-surface-alt/75 focus-visible:bg-surface-alt/75 focus-visible:underline focus-visible:outline-hidden dark:bg-surface-dark-alt dark:hover:bg-surface-dark-alt/75 dark:focus-visible:bg-surface-dark-alt/75"
aria-controls="accordionItemThree" x-on:click="isExpanded = ! isExpanded"
x-bind:class="isExpanded ? 'text-on-surface-strong dark:text-on-surface-dark-strong font-bold' : 'text-on-surface dark:text-on-surface-dark font-medium'"
x-bind:aria-expanded="isExpanded ? 'true' : 'false'">
What is the refund policy?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke="currentColor" class="size-5 shrink-0 transition" aria-hidden="true"
x-bind:class="isExpanded ? 'rotate-180' : ''">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div x-cloak x-show="isExpanded" id="accordionItemThree" role="region"
aria-labelledby="controlsAccordionItemThree" x-collapse>
<div class="p-4 text-sm sm:text-base text-pretty">
Please refer to our <a href="#"
class="underline underline-offset-2 text-primary dark:text-primary-dark">refund policy page</a> on
the website for detailed information regarding eligibility, timeframes, and the process for requesting a
refund.
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,59 @@
<div class="flex max-w-sm flex-col gap-4 border-outline bg-surface-alt p-6 text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark rounded-radius border">
<span id="voiceToneTitle" class="w-fit pl-0.5 text-sm font-medium">Image Style</span>
<div class="grid grid-cols-3 place-content-center gap-4" role="group" aria-label="image style">
<label for="imageStyleCustom" class="text-cente relative flex flex-col items-center">
<input type="file" id="imageStyleCustom" class="peer fixed opacity-0 size-0 appearance-none"/>
<div class="flex h-full w-full items-center justify-center border border-dashed border-on-surface peer-focus:outline-2 peer-focus:outline-offset-2 peer-focus:outline-primary dark:border-on-surface-dark dark:peer-focus:outline-primary-dark rounded-radius">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-10">
<path fill-rule="evenodd" d="M10.5 3.75a6 6 0 0 0-5.98 6.496A5.25 5.25 0 0 0 6.75 20.25H18a4.5 4.5 0 0 0 2.206-8.423 3.75 3.75 0 0 0-4.133-4.303A6.001 6.001 0 0 0 10.5 3.75Zm2.03 5.47a.75.75 0 0 0-1.06 0l-3 3a.75.75 0 1 0 1.06 1.06l1.72-1.72v4.94a.75.75 0 0 0 1.5 0v-4.94l1.72 1.72a.75.75 0 1 0 1.06-1.06l-3-3Z" clip-rule="evenodd"/>
</svg>
</div>
<span class="mt-1 text-xs capitalize">Upload</span>
</label>
<label for="imageStyleReal" class="text-center relative flex flex-col items-center" >
<input id="imageStyleReal" type="radio" class="peer appearance-none size-0" value="real" name="imageStyle" checked />
<img src="https://penguinui.s3.amazonaws.com/component-assets/image-styles/real.webp" class="object-cover rounded-radius peer-focus:outline-2 peer-focus:outline-offset-2 peer-focus:outline-primary dark:peer-focus:outline-primary-dark" alt="real" aria-hidden="true"/>
<span class="mt-1 text-xs capitalize">real</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="absolute right-1 top-1 size-5 fill-surface opacity-0 peer-checked:opacity-100 dark:fill-surface-dark" >
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd" />
</svg>
</label>
<label for="imageStyleWatercolor" class="text-center relative flex flex-col items-center" >
<input id="imageStyleWatercolor" type="radio" class="peer appearance-none size-0" value="watercolor" name="imageStyle" />
<img src="https://penguinui.s3.amazonaws.com/component-assets/image-styles/watercolor.webp" class="object-cover rounded-radius peer-focus:outline-2 peer-focus:outline-offset-2 peer-focus:outline-primary dark:peer-focus:outline-primary-dark" alt="watercolor" aria-hidden="true"/>
<span class="mt-1 text-xs capitalize">watercolor</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="absolute right-1 top-1 size-5 fill-surface opacity-0 peer-checked:opacity-100 dark:fill-surface-dark" >
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd" />
</svg>
</label>
<label for="imageStyle3d" class="text-center relative flex flex-col items-center" >
<input id="imageStyle3d" type="radio" class="peer appearance-none size-0" value="3d" name="imageStyle" />
<img src="https://penguinui.s3.amazonaws.com/component-assets/image-styles/3d.webp" class="object-cover rounded-radius peer-focus:outline-2 peer-focus:outline-offset-2 peer-focus:outline-primary dark:peer-focus:outline-primary-dark" alt="3d" aria-hidden="true"/>
<span class="mt-1 text-xs capitalize">3d</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="absolute right-1 top-1 size-5 fill-surface opacity-0 peer-checked:opacity-100 dark:fill-surface-dark" >
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd" />
</svg>
</label>
<label for="imageStyleIsometric" class="text-center relative flex flex-col items-center" >
<input id="imageStyleIsometric" type="radio" class="peer appearance-none size-0" value="isometric" name="imageStyle" />
<img src="https://penguinui.s3.amazonaws.com/component-assets/image-styles/isometric.webp" class="object-cover rounded-radius peer-focus:outline-2 peer-focus:outline-offset-2 peer-focus:outline-primary dark:peer-focus:outline-primary-dark" alt="isometric" aria-hidden="true"/>
<span class="mt-1 text-xs capitalize">isometric</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="absolute right-1 top-1 size-5 fill-surface opacity-0 peer-checked:opacity-100 dark:fill-surface-dark" >
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd" />
</svg>
</label>
<label for="imageStyleFlat" class="text-center relative flex flex-col items-center" >
<input id="imageStyleFlat" type="radio" class="peer appearance-none size-0" value="flat" name="imageStyle" />
<img src="https://penguinui.s3.amazonaws.com/component-assets/image-styles/flat.webp" class="object-cover rounded-radius peer-focus:outline-2 peer-focus:outline-offset-2 peer-focus:outline-primary dark:peer-focus:outline-primary-dark" alt="flat" aria-hidden="true"/>
<span class="mt-1 text-xs capitalize">flat</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="absolute right-1 top-1 size-5 fill-surface opacity-0 peer-checked:opacity-100 dark:fill-surface-dark" >
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd" />
</svg>
</label>
</div>
</div>

View File

@@ -0,0 +1,39 @@
<div class="flex w-full max-w-sm flex-col gap-4 rounded-radius border border-outline bg-surface-alt p-4 sm:p-6 text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark" role="group" aria-labelledby="aiModelTitle">
<span id="aiModelTitle" class="w-fit px-4 text-sm font-medium">Model</span>
<label for="model3" class="flex items-center justify-between gap-2 px-4 py-2 rounded-radius hover:bg-surface-dark/5 has-focus-visible:bg-surface-dark/5 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-primary dark:hover:bg-surface/5 dark:has-focus-visible:bg-surface/5 dark:has-focus-visible:outline-primary-dark">
<div class="flex items-center gap-4">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 shrink-0 fill-primary dark:fill-primary-dark" aria-hidden="true">
<path d="M15.98 1.804a1 1 0 0 0-1.96 0l-.24 1.192a1 1 0 0 1-.784.785l-1.192.238a1 1 0 0 0 0 1.962l1.192.238a1 1 0 0 1 .785.785l.238 1.192a1 1 0 0 0 1.962 0l.238-1.192a1 1 0 0 1 .785-.785l1.192-.238a1 1 0 0 0 0-1.962l-1.192-.238a1 1 0 0 1-.785-.785l-.238-1.192ZM6.949 5.684a1 1 0 0 0-1.898 0l-.683 2.051a1 1 0 0 1-.633.633l-2.051.683a1 1 0 0 0 0 1.898l2.051.684a1 1 0 0 1 .633.632l.683 2.051a1 1 0 0 0 1.898 0l.683-2.051a1 1 0 0 1 .633-.633l2.051-.683a1 1 0 0 0 0-1.898l-2.051-.683a1 1 0 0 1-.633-.633L6.95 5.684ZM13.949 13.684a1 1 0 0 0-1.898 0l-.184.551a1 1 0 0 1-.632.633l-.551.183a1 1 0 0 0 0 1.898l.551.183a1 1 0 0 1 .633.633l.183.551a1 1 0 0 0 1.898 0l.184-.551a1 1 0 0 1 .632-.633l.551-.183a1 1 0 0 0 0-1.898l-.551-.184a1 1 0 0 1-.633-.632l-.183-.551Z"/>
</svg>
<div class="text-left">
<p class="text-sm font-bold">Pengu AI 3.1</p>
<p class="text-xs text-on-surface dark:text-on-surface-dark">The most advanced AI model</p>
</div>
</div>
<input id="model3" type="radio" class="peer appearance-none" name="model" value="3.1" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
class="size-6 opacity-0 peer-checked:opacity-100" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd"/>
</svg>
</label>
<label for="model2" class="flex items-center justify-between gap-2 px-4 py-2 rounded-radius hover:bg-surface-dark/5 has-focus-visible:bg-surface-dark/5 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-primary dark:hover:bg-surface/5 dark:has-focus-visible:bg-surface/5 dark:has-focus-visible:outline-primary-dark">
<div class="flex items-center gap-4">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 shrink-0 fill-secondary dark:fill-secondary-dark" aria-hidden="true">
<path d="M15.98 1.804a1 1 0 0 0-1.96 0l-.24 1.192a1 1 0 0 1-.784.785l-1.192.238a1 1 0 0 0 0 1.962l1.192.238a1 1 0 0 1 .785.785l.238 1.192a1 1 0 0 0 1.962 0l.238-1.192a1 1 0 0 1 .785-.785l1.192-.238a1 1 0 0 0 0-1.962l-1.192-.238a1 1 0 0 1-.785-.785l-.238-1.192ZM6.949 5.684a1 1 0 0 0-1.898 0l-.683 2.051a1 1 0 0 1-.633.633l-2.051.683a1 1 0 0 0 0 1.898l2.051.684a1 1 0 0 1 .633.632l.683 2.051a1 1 0 0 0 1.898 0l.683-2.051a1 1 0 0 1 .633-.633l2.051-.683a1 1 0 0 0 0-1.898l-2.051-.683a1 1 0 0 1-.633-.633L6.95 5.684ZM13.949 13.684a1 1 0 0 0-1.898 0l-.184.551a1 1 0 0 1-.632.633l-.551.183a1 1 0 0 0 0 1.898l.551.183a1 1 0 0 1 .633.633l.183.551a1 1 0 0 0 1.898 0l.184-.551a1 1 0 0 1 .632-.633l.551-.183a1 1 0 0 0 0-1.898l-.551-.184a1 1 0 0 1-.633-.632l-.183-.551Z"/>
</svg>
<div class="text-left">
<p class="text-sm font-bold">Pengu AI 2.4</p>
<p class="text-xs text-on-surface dark:text-on-surface-dark">The older AI model</p>
</div>
</div>
<input id="model2" type="radio" class="peer appearance-none" name="model" value="2.4" checked />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
class="size-6 opacity-0 peer-checked:opacity-100" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd"/>
</svg>
</label>
<button type="button" class="mx-4 mt-2 whitespace-nowrap bg-primary px-4 py-2 text-center text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 dark:bg-primary-dark dark:text-on-primary-dark dark:has-focus-visible:bg-surface/5 dark:focus-visible:outline-primary-dark rounded-radius">Upgrade to Pro</button>
</div>

View File

@@ -0,0 +1,45 @@
<div class="flex flex-col gap-2 text-on-surface dark:text-on-surface-dark" role="group" aria-labelledby="voiceToneTitle">
<span id="voiceToneTitle" class="w-fit pl-0.5 text-sm font-medium">Voice Tone</span>
<div class="flex flex-wrap items-center gap-4">
<label class="flex border border-transparent bg-surface-alt px-2 py-1.5 text-xs font-medium has-checked:border-primary has-checked:text-primary dark:bg-surface-dark-alt dark:has-checked:border-primary-dark dark:has-checked:text-primary-dark rounded-radius has-focus-visible:outline-2 has-focus-visible:outline-primary has-focus-visible:outline-offset-2 dark:has-focus-visible:outline-primary-dark" for="checkToneFriendly">
<input type="checkbox" class="appearance-none" id="checkToneFriendly" value="friendly" name="tone" />
<span class="flex items-center gap-2">
<span class="text-base" aria-hidden="true">🤗</span>
Friendly
</span>
</label>
<label class="flex border border-transparent bg-surface-alt px-2 py-1.5 text-xs font-medium has-checked:border-primary has-checked:text-primary dark:bg-surface-dark-alt dark:has-checked:border-primary-dark dark:has-checked:text-primary-dark rounded-radius has-focus-visible:outline-2 has-focus-visible:outline-primary has-focus-visible:outline-offset-2 dark:has-focus-visible:outline-primary-dark" for="checkToneProfessional">
<input type="checkbox" class="appearance-none" id="checkToneProfessional" value="professional" name="tone" />
<span class="flex items-center gap-2">
<span class="text-base" aria-hidden="true">🤓</span>
Professional
</span>
</label>
<label class="flex border border-transparent bg-surface-alt px-2 py-1.5 text-xs font-medium has-checked:border-primary has-checked:text-primary dark:bg-surface-dark-alt dark:has-checked:border-primary-dark dark:has-checked:text-primary-dark rounded-radius has-focus-visible:outline-2 has-focus-visible:outline-primary has-focus-visible:outline-offset-2 dark:has-focus-visible:outline-primary-dark" for="checkToneCheerful">
<input type="checkbox" class="appearance-none" id="checkToneCheerful" value="cheerful" name="tone" />
<span class="flex items-center gap-2">
<span class="text-base" aria-hidden="true">🤩</span>
Cheerful
</span>
</label>
<label class="flex border border-transparent bg-surface-alt px-2 py-1.5 text-xs font-medium has-checked:border-primary has-checked:text-primary dark:bg-surface-dark-alt dark:has-checked:border-primary-dark dark:has-checked:text-primary-dark rounded-radius has-focus-visible:outline-2 has-focus-visible:outline-primary has-focus-visible:outline-offset-2 dark:has-focus-visible:outline-primary-dark" for="checkToneFunny">
<input type="checkbox" class="appearance-none" id="checkToneFunny" value="funny" name="tone" />
<span class="flex items-center gap-2">
<span class="text-base" aria-hidden="true">🤭</span>
Funny
</span>
</label>
<label class="flex border border-transparent bg-surface-alt px-2 py-1.5 text-xs font-medium has-checked:border-primary has-checked:text-primary dark:bg-surface-dark-alt dark:has-checked:border-primary-dark dark:has-checked:text-primary-dark rounded-radius has-focus-visible:outline-2 has-focus-visible:outline-primary has-focus-visible:outline-offset-2 dark:has-focus-visible:outline-primary-dark" for="checkTonePersuasive">
<input type="checkbox" class="appearance-none" id="checkTonePersuasive" value="persuasive" name="tone" />
<span class="flex items-center gap-2">
<span class="text-base" aria-hidden="true">😎</span>
Persuasive
</span>
</label>
</div>
</div>

View File

@@ -0,0 +1,65 @@
<div class="flex flex-col gap-4">
<!-- User's Chat -->
<div class="w-full max-w-2xl border-outline bg-surface-alt p-6 text-left dark:border-outline-dark dark:bg-surface-dark-alt rounded-radius border" >
<div class="flex items-center gap-2 text-on-surface-strong dark:text-on-surface-dark-strong">
<img src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" class="size-8 rounded-full object-cover" alt="User avatar"/>
<span class="text-sm font-bold">Alice Brown</span>
</div>
<p class="text-pretty sm:pl-10 mt-4 sm:mt-0 text-sm text-on-surface dark:text-on-surface-dark">write a short paragraph about penguin.</p>
<!-- Actions -->
<div class="mt-2 flex items-center gap-2 sm:pl-10">
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Edit" aria-label="Edit your input">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-4" >
<path d="m5.433 13.917 1.262-3.155A4 4 0 0 1 7.58 9.42l6.92-6.918a2.121 2.121 0 0 1 3 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 0 1-.65-.65Z"/>
<path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0 0 10 3H4.75A2.75 2.75 0 0 0 2 5.75v9.5A2.75 2.75 0 0 0 4.75 18h9.5A2.75 2.75 0 0 0 17 15.25V10a.75.75 0 0 0-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5Z"/>
</svg>
</button>
</div>
</div>
<!-- AI's Response -->
<div class="w-full max-w-2xl border-outline bg-surface-alt p-6 text-left dark:border-outline-dark dark:bg-surface-dark-alt rounded-radius border" >
<div class="flex items-center gap-2 text-on-surface-strong dark:text-on-surface-dark-strong">
<span class="flex size-8 items-center justify-center rounded-full bg-primary text-on-primary dark:bg-primary-dark dark:text-on-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-5">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
</span>
<span class="text-sm font-bold">Pengu AI</span>
</div>
<p class="text-pretty sm:pl-10 mt-4 sm:mt-0 text-sm text-on-surface dark:text-on-surface-dark">
The penguin is a fascinating bird perfectly adapted to life in the cold Antarctic regions.
With its distinctive black and white plumage, streamlined body, and flipper-like wings, the penguin
is an agile swimmer and expert diver, hunting for fish, krill, and squid underwater. Penguins are
highly social animals, often gathering in large colonies for breeding and protection. They have a unique
waddling gait on land but are graceful and swift in the water, using their flippers to propel themselves
through the icy seas. Known for their resilience in harsh environments, penguins evoke a sense of curiosity
and admiration for their ability to thrive in one of Earth's most extreme habitats.
</p>
<!-- Actions -->
<div class="mt-2 flex items-center gap-2 sm:pl-10">
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Read Aloud" aria-label="Read Aloud" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4" aria-hidden="true">
<path d="M10.5 3.75a.75.75 0 0 0-1.264-.546L5.203 7H2.667a.75.75 0 0 0-.7.48A6.985 6.985 0 0 0 1.5 10c0 .887.165 1.737.468 2.52.111.29.39.48.7.48h2.535l4.033 3.796a.75.75 0 0 0 1.264-.546V3.75ZM16.45 5.05a.75.75 0 0 0-1.06 1.061 5.5 5.5 0 0 1 0 7.778.75.75 0 0 0 1.06 1.06 7 7 0 0 0 0-9.899Z"/>
<path d="M14.329 7.172a.75.75 0 0 0-1.061 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 0 0 1.06 1.06 4 4 0 0 0 0-5.656Z"/>
</svg>
</button>
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Copy" aria-label="Copy" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4" aria-hidden="true">
<path fill-rule="evenodd" d="M13.887 3.182c.396.037.79.08 1.183.128C16.194 3.45 17 4.414 17 5.517V16.75A2.25 2.25 0 0 1 14.75 19h-9.5A2.25 2.25 0 0 1 3 16.75V5.517c0-1.103.806-2.068 1.93-2.207.393-.048.787-.09 1.183-.128A3.001 3.001 0 0 1 9 1h2c1.373 0 2.531.923 2.887 2.182ZM7.5 4A1.5 1.5 0 0 1 9 2.5h2A1.5 1.5 0 0 1 12.5 4v.5h-5V4Z" clip-rule="evenodd"/>
</svg>
</button>
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Like" aria-label="Like" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4" aria-hidden="true">
<path d="M1 8.25a1.25 1.25 0 1 1 2.5 0v7.5a1.25 1.25 0 1 1-2.5 0v-7.5ZM11 3V1.7c0-.268.14-.526.395-.607A2 2 0 0 1 14 3c0 .995-.182 1.948-.514 2.826-.204.54.166 1.174.744 1.174h2.52c1.243 0 2.261 1.01 2.146 2.247a23.864 23.864 0 0 1-1.341 5.974C17.153 16.323 16.072 17 14.9 17h-3.192a3 3 0 0 1-1.341-.317l-2.734-1.366A3 3 0 0 0 6.292 15H5V8h.963c.685 0 1.258-.483 1.612-1.068a4.011 4.011 0 0 1 2.166-1.73c.432-.143.853-.386 1.011-.814.16-.432.248-.9.248-1.388Z"/>
</svg>
</button>
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="More settings" aria-label="More settings" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4" aria-hidden="true">
<path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
</svg>
</button>
</div>
</div>

View File

@@ -0,0 +1,56 @@
<div class="flex flex-col gap-4 w-fit">
<!-- User's Chat -->
<div class="w-full max-w-2xl border-outline bg-surface-alt p-6 text-left dark:border-outline-dark dark:bg-surface-dark-alt rounded-radius border" >
<div class="flex items-center gap-2 text-on-surface-strong dark:text-on-surface-dark-strong">
<img src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" class="size-8 rounded-full object-cover" alt="User avatar"/>
<span class="text-sm font-bold">Alice Brown</span>
</div>
<p class="text-pretty sm:pl-10 mt-4 sm:mt-0 text-sm text-on-surface dark:text-on-surface-dark">what is a developer?</p>
<!-- Actions -->
<div class="mt-2 flex items-center gap-2 sm:pl-10">
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Edit" aria-label="Edit your input">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-4" >
<path d="m5.433 13.917 1.262-3.155A4 4 0 0 1 7.58 9.42l6.92-6.918a2.121 2.121 0 0 1 3 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 0 1-.65-.65Z"/>
<path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0 0 10 3H4.75A2.75 2.75 0 0 0 2 5.75v9.5A2.75 2.75 0 0 0 4.75 18h9.5A2.75 2.75 0 0 0 17 15.25V10a.75.75 0 0 0-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5Z"/>
</svg>
</button>
</div>
</div>
<p class="text-sm text-center text-on-surface dark:text-on-surface-dark">Which response do you prefer?</p>
<!-- AI's Response -->
<div class="flex w-full max-w-2xl gap-4 overflow-x-auto pb-4">
<!-- Response 1 -->
<div class="w-full min-w-[80%] md:min-w-0 border-outline bg-surface-alt p-6 text-left dark:border-outline-dark dark:bg-surface-dark-alt rounded-radius border" >
<div class="flex items-center gap-2 text-on-surface-strong dark:text-on-surface-dark-strong">
<span class="flex size-8 items-center justify-center rounded-full bg-primary text-on-primary dark:bg-primary-dark dark:text-on-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-5">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
</span>
<span class="text-sm font-bold">Response 1</span>
</div>
<p class="text-pretty sm:pl-10 mt-4 sm:mt-0 text-sm text-on-surface dark:text-on-surface-dark">
A magician who turns caffeine into code, pizza into programs, and stress into syntax errors.
</p>
</div>
<!-- Response 2 -->
<div class="w-full min-w-[80%] md:min-w-0 border-outline bg-surface-alt p-6 text-left dark:border-outline-dark dark:bg-surface-dark-alt rounded-radius border" >
<div class="flex items-center gap-2 text-on-surface-strong dark:text-on-surface-dark-strong">
<span class="flex size-8 items-center justify-center rounded-full bg-primary text-on-primary dark:bg-primary-dark dark:text-on-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-5">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
</span>
<span class="text-sm font-bold">Response 2</span>
</div>
<p class="text-pretty sm:pl-10 mt-4 sm:mt-0 text-sm text-on-surface dark:text-on-surface-dark">
Someone who talks to computers in their own language and occasionally gets a response.
</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,82 @@
<div class="sm:grid-cols-2 grid-cols-1 grid w-full max-w-2xl gap-6 border-outline bg-surface-alt p-6 dark:border-outline-dark dark:bg-surface-dark-alt rounded-radius border">
<div class="group relative">
<img class="object-cover rounded-radius" src="https://res.cloudinary.com/ds8pgw1pf/image/upload/penguinui/component-assets/ai-fantasy-1.webp" alt="fantasy character version 1" />
<button class="absolute right-4 top-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Download" aria-label="download">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"/>
<path d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"/>
</svg>
</button>
<button class="absolute right-4 bottom-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Share" aria-label="Share">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M12 6a2 2 0 1 0-1.994-1.842L5.323 6.5a2 2 0 1 0 0 3l4.683 2.342a2 2 0 1 0 .67-1.342L5.995 8.158a2.03 2.03 0 0 0 0-.316L10.677 5.5c.353.311.816.5 1.323.5Z"/>
</svg>
</button>
<button class="absolute left-4 bottom-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Report" aria-label="Report">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M2.75 2a.75.75 0 0 0-.75.75v10.5a.75.75 0 0 0 1.5 0v-2.624l.33-.083A6.044 6.044 0 0 1 8 11c1.29.645 2.77.807 4.17.457l1.48-.37a.462.462 0 0 0 .35-.448V3.56a.438.438 0 0 0-.544-.425l-1.287.322C10.77 3.808 9.291 3.646 8 3a6.045 6.045 0 0 0-4.17-.457l-.34.085A.75.75 0 0 0 2.75 2Z"/>
</svg>
</button>
</div>
<div class="group relative">
<img class="object-cover rounded-radius" src="https://res.cloudinary.com/ds8pgw1pf/image/upload/penguinui/component-assets/ai-fantasy-2.webp" alt="fantasy character version 2" />
<button class="absolute right-4 top-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Download" aria-label="download">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"/>
<path d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"/>
</svg>
</button>
<button class="absolute right-4 bottom-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Share" aria-label="Share">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M12 6a2 2 0 1 0-1.994-1.842L5.323 6.5a2 2 0 1 0 0 3l4.683 2.342a2 2 0 1 0 .67-1.342L5.995 8.158a2.03 2.03 0 0 0 0-.316L10.677 5.5c.353.311.816.5 1.323.5Z"/>
</svg>
</button>
<button class="absolute left-4 bottom-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Report" aria-label="Report">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M2.75 2a.75.75 0 0 0-.75.75v10.5a.75.75 0 0 0 1.5 0v-2.624l.33-.083A6.044 6.044 0 0 1 8 11c1.29.645 2.77.807 4.17.457l1.48-.37a.462.462 0 0 0 .35-.448V3.56a.438.438 0 0 0-.544-.425l-1.287.322C10.77 3.808 9.291 3.646 8 3a6.045 6.045 0 0 0-4.17-.457l-.34.085A.75.75 0 0 0 2.75 2Z"/>
</svg>
</button>
</div>
<div class="group relative">
<img class="object-cover rounded-radius" src="https://res.cloudinary.com/ds8pgw1pf/image/upload/penguinui/component-assets/ai-fantasy-3.webp" alt="fantasy character version 3" />
<button class="absolute right-4 top-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Download" aria-label="download">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"/>
<path d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"/>
</svg>
</button>
<button class="absolute right-4 bottom-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Share" aria-label="Share">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M12 6a2 2 0 1 0-1.994-1.842L5.323 6.5a2 2 0 1 0 0 3l4.683 2.342a2 2 0 1 0 .67-1.342L5.995 8.158a2.03 2.03 0 0 0 0-.316L10.677 5.5c.353.311.816.5 1.323.5Z"/>
</svg>
</button>
<button class="absolute left-4 bottom-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Report" aria-label="Report">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M2.75 2a.75.75 0 0 0-.75.75v10.5a.75.75 0 0 0 1.5 0v-2.624l.33-.083A6.044 6.044 0 0 1 8 11c1.29.645 2.77.807 4.17.457l1.48-.37a.462.462 0 0 0 .35-.448V3.56a.438.438 0 0 0-.544-.425l-1.287.322C10.77 3.808 9.291 3.646 8 3a6.045 6.045 0 0 0-4.17-.457l-.34.085A.75.75 0 0 0 2.75 2Z"/>
</svg>
</button>
</div>
<div class="group relative">
<img class="object-cover rounded-radius" src="https://res.cloudinary.com/ds8pgw1pf/image/upload/penguinui/component-assets/ai-fantasy-4.webp" alt="fantasy character version 4" />
<button class="absolute right-4 top-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Download" aria-label="download">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"/>
<path d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"/>
</svg>
</button>
<button class="absolute right-4 bottom-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Share" aria-label="Share">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M12 6a2 2 0 1 0-1.994-1.842L5.323 6.5a2 2 0 1 0 0 3l4.683 2.342a2 2 0 1 0 .67-1.342L5.995 8.158a2.03 2.03 0 0 0 0-.316L10.677 5.5c.353.311.816.5 1.323.5Z"/>
</svg>
</button>
<button class="absolute left-4 bottom-3 rounded-full bg-surface/75 p-1.5 text-on-surface opacity-0 transition hover:bg-surface/85 focus-visible:opacity-100 active:outline-offset-0 focus-visible:outline-offset-2 group-hover:opacity-100 group-focus:opacity-100 dark:bg-surface-dark/75 dark:text-on-surface-dark dark:hover:bg-surface-dark/85" title="Report" aria-label="Report">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M2.75 2a.75.75 0 0 0-.75.75v10.5a.75.75 0 0 0 1.5 0v-2.624l.33-.083A6.044 6.044 0 0 1 8 11c1.29.645 2.77.807 4.17.457l1.48-.37a.462.462 0 0 0 .35-.448V3.56a.438.438 0 0 0-.544-.425l-1.287.322C10.77 3.808 9.291 3.646 8 3a6.045 6.045 0 0 0-4.17-.457l-.34.085A.75.75 0 0 0 2.75 2Z"/>
</svg>
</button>
</div>
</div>

View File

@@ -0,0 +1,74 @@
<div class="flex flex-col gap-4">
<!-- User's Chat -->
<div class="w-full max-w-2xl border-outline bg-surface-alt p-6 text-left dark:border-outline-dark dark:bg-surface-dark-alt rounded-radius border" >
<div class="flex items-center gap-2 text-on-surface-strong dark:text-on-surface-dark-strong">
<img src="https://penguinui.s3.amazonaws.com/component-assets/avatar-1.webp" class="size-8 rounded-full object-cover" alt="User avatar"/>
<span class="text-sm font-bold">Bob Johnson</span>
</div>
<p class="text-pretty sm:pl-10 mt-4 sm:mt-0 text-sm text-on-surface dark:text-on-surface-dark">Generate a code for "Hellow World" in Javascript</p>
<!-- Actions -->
<div class="mt-2 flex items-center gap-2 sm:pl-10">
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Edit" aria-label="Edit your input">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-4" >
<path d="m5.433 13.917 1.262-3.155A4 4 0 0 1 7.58 9.42l6.92-6.918a2.121 2.121 0 0 1 3 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 0 1-.65-.65Z"/>
<path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0 0 10 3H4.75A2.75 2.75 0 0 0 2 5.75v9.5A2.75 2.75 0 0 0 4.75 18h9.5A2.75 2.75 0 0 0 17 15.25V10a.75.75 0 0 0-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5Z"/>
</svg>
</button>
</div>
</div>
<!-- AI's Response -->
<div class="w-full max-w-2xl border-outline bg-surface-alt p-6 text-left dark:border-outline-dark dark:bg-surface-dark-alt rounded-radius border" >
<div class="flex items-center gap-2 text-on-surface-strong dark:text-on-surface-dark-strong">
<span class="flex size-8 items-center justify-center rounded-full bg-primary text-on-primary dark:bg-primary-dark dark:text-on-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-5">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
</span>
<span class="text-sm font-bold">Pengu AI</span>
</div>
<p class="text-pretty sm:pl-10 mt-4 sm:mt-0 text-sm text-on-surface dark:text-on-surface-dark">
Certainly! Below is a simple JavaScript code snippet to print "Hello World" to the console:
</p>
<div class="relative max-w-full overflow-hidden">
<button class="absolute right-2 top-5 rounded-full p-1 text-on-surface-dark/75 hover:text-on-surface-dark focus:outline-hidden focus-visible:text-on-surface-dark focus-visible:outline-offset-0 focus-visible:outline-primary-dark active:-outline-offset-2" title="Copy Code" aria-label="Copy Code"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4" aria-hidden="true">
<path fill-rule="evenodd" d="M13.887 3.182c.396.037.79.08 1.183.128C16.194 3.45 17 4.414 17 5.517V16.75A2.25 2.25 0 0 1 14.75 19h-9.5A2.25 2.25 0 0 1 3 16.75V5.517c0-1.103.806-2.068 1.93-2.207.393-.048.787-.09 1.183-.128A3.001 3.001 0 0 1 9 1h2c1.373 0 2.531.923 2.887 2.182ZM7.5 4A1.5 1.5 0 0 1 9 2.5h2A1.5 1.5 0 0 1 12.5 4v.5h-5V4Z" clip-rule="evenodd"/>
</svg>
</button>
<pre class="sm:ml-10 scroll-on my-4 overflow-x-auto bg-surface-dark p-4 text-sm text-on-surface-dark rounded-radius">
&lt;<span class="text-pink-400">script</span>&gt;
<span class="text-pink-400">console</span>.log("<span class="text-blue-400">Hello World!</span>")
&lt;/<span class="text-pink-400">script</span>&gt;
</pre>
</div>
<p class="text-pretty sm:pl-10 mt-4 sm:mt-0 text-sm text-on-surface dark:text-on-surface-dark">
Is there anything else I can help with?
</p>
<!-- Actions -->
<div class="mt-2 flex items-center gap-2 sm:pl-10">
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Read Aloud" aria-label="Read Aloud" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4" aria-hidden="true">
<path d="M10.5 3.75a.75.75 0 0 0-1.264-.546L5.203 7H2.667a.75.75 0 0 0-.7.48A6.985 6.985 0 0 0 1.5 10c0 .887.165 1.737.468 2.52.111.29.39.48.7.48h2.535l4.033 3.796a.75.75 0 0 0 1.264-.546V3.75ZM16.45 5.05a.75.75 0 0 0-1.06 1.061 5.5 5.5 0 0 1 0 7.778.75.75 0 0 0 1.06 1.06 7 7 0 0 0 0-9.899Z"/>
<path d="M14.329 7.172a.75.75 0 0 0-1.061 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 0 0 1.06 1.06 4 4 0 0 0 0-5.656Z"/>
</svg>
</button>
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Copy" aria-label="Copy" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4" aria-hidden="true">
<path fill-rule="evenodd" d="M13.887 3.182c.396.037.79.08 1.183.128C16.194 3.45 17 4.414 17 5.517V16.75A2.25 2.25 0 0 1 14.75 19h-9.5A2.25 2.25 0 0 1 3 16.75V5.517c0-1.103.806-2.068 1.93-2.207.393-.048.787-.09 1.183-.128A3.001 3.001 0 0 1 9 1h2c1.373 0 2.531.923 2.887 2.182ZM7.5 4A1.5 1.5 0 0 1 9 2.5h2A1.5 1.5 0 0 1 12.5 4v.5h-5V4Z" clip-rule="evenodd"/>
</svg>
</button>
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Like" aria-label="Like" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4" aria-hidden="true">
<path d="M1 8.25a1.25 1.25 0 1 1 2.5 0v7.5a1.25 1.25 0 1 1-2.5 0v-7.5ZM11 3V1.7c0-.268.14-.526.395-.607A2 2 0 0 1 14 3c0 .995-.182 1.948-.514 2.826-.204.54.166 1.174.744 1.174h2.52c1.243 0 2.261 1.01 2.146 2.247a23.864 23.864 0 0 1-1.341 5.974C17.153 16.323 16.072 17 14.9 17h-3.192a3 3 0 0 1-1.341-.317l-2.734-1.366A3 3 0 0 0 6.292 15H5V8h.963c.685 0 1.258-.483 1.612-1.068a4.011 4.011 0 0 1 2.166-1.73c.432-.143.853-.386 1.011-.814.16-.432.248-.9.248-1.388Z"/>
</svg>
</button>
<button class="rounded-full p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="More settings" aria-label="More settings" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4" aria-hidden="true">
<path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
</svg>
</button>
</div>
</div>

View File

@@ -0,0 +1,31 @@
<div x-data="{
copiedToClipboard: false,
copyToClipboard() {
navigator.clipboard
.writeText($refs.targetText.textContent)
.then(() => {
this.copiedToClipboard = true
})
.catch((err) => {
this.copiedToClipboard = false
})
},
}" class="flex flex-col gap-4 border border-outline rounded-radius bg-surface-alt p-6 text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark">
<pre x-ref="targetText" class="w-full whitespace-normal">
<p>Here is a joke about penguins:</p>
<p>Why don't penguins like talking to strangers at parties?</p>
<p>Because they find it too ice-breaking!</p>
</pre>
<button class="rounded-full w-fit p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Copy" aria-label="Copy" x-on:click="copyToClipboard()" x-on:click.away="copiedToClipboard = false">
<span class="sr-only" x-text="copiedToClipboard ? 'copied' : 'copy the response to clipboard'"></span>
<svg x-show="!copiedToClipboard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4" aria-hidden="true">
<path fill-rule="evenodd" d="M13.887 3.182c.396.037.79.08 1.183.128C16.194 3.45 17 4.414 17 5.517V16.75A2.25 2.25 0 0 1 14.75 19h-9.5A2.25 2.25 0 0 1 3 16.75V5.517c0-1.103.806-2.068 1.93-2.207.393-.048.787-.09 1.183-.128A3.001 3.001 0 0 1 9 1h2c1.373 0 2.531.923 2.887 2.182ZM7.5 4A1.5 1.5 0 0 1 9 2.5h2A1.5 1.5 0 0 1 12.5 4v.5h-5V4Z" clip-rule="evenodd"/>
</svg>
<svg x-show="copiedToClipboard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4 fill-success">
<path fill-rule="evenodd" d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z" clip-rule="evenodd"/>
<path fill-rule="evenodd" d="M2 7a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7Zm6.585 1.08a.75.75 0 0 1 .336 1.005l-1.75 3.5a.75.75 0 0 1-1.16.234l-1.75-1.5a.75.75 0 0 1 .977-1.139l1.02.875 1.321-2.64a.75.75 0 0 1 1.006-.336Z" clip-rule="evenodd"/>
</svg>
</button>
</div>

View File

@@ -0,0 +1,9 @@
<div class="relative max-w-md">
<img src="/your-file" alt="variation 1 - image in webp format" class="object-cover rounded-radius" />
<a role="button" href="/your-file" download="penguin-ai-robot" class="absolute right-2.5 top-2.5 rounded-radius z-10 bg-surface-alt/75 p-1 text-on-surface dark:bg-surface-dark-alt/75 dark:text-on-surface-dark">
<span class="sr-only">download variation 1</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</a>
</div>

View File

@@ -0,0 +1,122 @@
<div x-data="{
fallbackModalIsOpen: false,
copiedToClipboard: false,
share() {
// check if web share API is available
if (navigator.share) {
navigator.share({
title: document.title,
text: 'Check out this site',
url: window.location.href,
})
} else {
this.fallbackModalIsOpen = true
}
},
copyUrlToClipboard(url) {
navigator.clipboard
.writeText(url)
.then(() => {
this.copiedToClipboard = true
})
.catch((err) => {
this.copiedToClipboard = false
})
},
}">
<button title="Share" x-on:click="share()" class="rounded-full p-1 text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:outline-offset-0 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark">
<span class="sr-only">share</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-6">
<path fill-rule="evenodd" d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z" clip-rule="evenodd" />
</svg>
</button>
<!-- Modal -->
<div x-cloak x-show="fallbackModalIsOpen" class="fixed inset-0 z-100 flex items-end justify-center bg-black/30 p-4 pb-8 sm:items-center lg:p-8" role="dialog" aria-labelledby="sharetModalTitle" aria-modal="true" x-on:click.self="fallbackModalIsOpen = false" x-on:keydown.esc.window="fallbackModalIsOpen = false" x-transition.opacity.duration.200ms x-trap.inert.noscroll="fallbackModalIsOpen">
<div x-show="fallbackModalIsOpen" class="flex w-full max-w-lg flex-col gap-4 rounded-radius border border-outline overflow-hidden bg-surface text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark" x-transition:enter="transition delay-100 duration-200 ease-out motion-reduce:transition-opacity" x-transition:enter-end="scale-100 opacity-100" x-transition:enter-start="scale-50 opacity-0">
<!-- Dialog Header -->
<div class="flex items-center justify-between border-b border-outline bg-surface-alt/60 p-4 dark:border-outline-dark dark:bg-surface-dark/20">
<h3 id="sharetModalTitle" class="font-semibold tracking-wide text-on-surface-strong dark:text-on-surface-dark-strong">Share</h3>
<button aria-label="close modal" x-on:click="fallbackModalIsOpen = false">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-5">
<path fill-rule="evenodd" d="M5.47 5.47a.75.75 0 011.06 0L12 10.94l5.47-5.47a.75.75 0 111.06 1.06L13.06 12l5.47 5.47a.75.75 0 11-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 01-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 010-1.06z" clip-rule="evenodd" />
</svg>
</button>
</div>
<!-- Dialog Body -->
<div class="flex flex-col gap-8 px-4 py-8">
<!-- Social Icons -->
<div class="grid grid-cols-3 gap-6 px-4 sm:grid-cols-5 sm:gap-4">
<!-- X - Twitter -->
<a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.penguinui.com%2F&text=UI%20Components%20for%20Tailwind%20CSS%20and%20Alpine%20JS" class="flex flex-col items-center justify-center gap-1.5 text-white" target="_blank">
<div class="w-fits flex items-center justify-center size-10 rounded-full bg-black p-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-5">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z" />
</svg>
</div>
<span class="whitespace-nowrap text-xs text-on-surface dark:text-on-surface-dark">X(Twitter)</span>
</a>
<!-- Facebook -->
<a href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.penguinui.com%2F" class="flex flex-col items-center justify-center gap-1.5 text-white" target="_blank">
<div class="w-fits flex items-center justify-center size-10 rounded-full bg-blue-500 p-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-6">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951" />
</svg>
</div>
<span class="whitespace-nowrap text-xs text-on-surface dark:text-on-surface-dark">Facebook</span>
</a>
<!-- Reddit -->
<a href="http://www.reddit.com/submit?url=https%3A%2F%2Fwww.penguinui.com%2F&title=UI%20Components%20for%20Tailwind%20CSS%20and%20Alpine%20JS" class="flex flex-col items-center justify-center gap-1.5 text-white" target="_blank">
<div class="w-fits flex items-center justify-center size-10 rounded-full bg-orange-600 p-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-7">
<path d="M6.167 8a.83.83 0 0 0-.83.83c0 .459.372.84.83.831a.831.831 0 0 0 0-1.661m1.843 3.647c.315 0 1.403-.038 1.976-.611a.23.23 0 0 0 0-.306.213.213 0 0 0-.306 0c-.353.363-1.126.487-1.67.487-.545 0-1.308-.124-1.671-.487a.213.213 0 0 0-.306 0 .213.213 0 0 0 0 .306c.564.563 1.652.61 1.977.61zm.992-2.807c0 .458.373.83.831.83s.83-.381.83-.83a.831.831 0 0 0-1.66 0z" />
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.828-1.165c-.315 0-.602.124-.812.325-.801-.573-1.9-.945-3.121-.993l.534-2.501 1.738.372a.83.83 0 1 0 .83-.869.83.83 0 0 0-.744.468l-1.938-.41a.2.2 0 0 0-.153.028.2.2 0 0 0-.086.134l-.592 2.788c-1.24.038-2.358.41-3.17.992-.21-.2-.496-.324-.81-.324a1.163 1.163 0 0 0-.478 2.224q-.03.17-.029.353c0 1.795 2.091 3.256 4.669 3.256s4.668-1.451 4.668-3.256c0-.114-.01-.238-.029-.353.401-.181.688-.592.688-1.069 0-.65-.525-1.165-1.165-1.165" />
</svg>
</div>
<span class="whitespace-nowrap text-xs text-on-surface dark:text-on-surface-dark">Reddit</span>
</a>
<!-- LinkedIn -->
<a href="http://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fwww.penguinui.com%2F&title=UI%20Components%20for%20Tailwind%20CSS%20and%20Alpine%20JS" class="flex flex-col items-center justify-center gap-1.5 text-white" target="_blank">
<div class="w-fits flex items-center justify-center size-10 rounded-full bg-blue-700 p-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-5">
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z" />
</svg>
</div>
<span class="whitespace-nowrap text-xs text-on-surface dark:text-on-surface-dark">Linkedin</span>
</a>
<!-- Email -->
<a href="mailto:?subject=Check out Penguin UI&body=Check out this cool UI components library http://www.penguinui.com." class="flex flex-col items-center justify-center gap-1.5" target="_blank">
<div class="w-fits flex items-center justify-center size-10 rounded-full bg-primary dark:bg-primary-dark p-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-6 fill-on-primary dark:fill-on-primary-dark">
<path d="M1.5 8.67v8.58a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V8.67l-8.928 5.493a3 3 0 0 1-3.144 0L1.5 8.67Z" />
<path d="M22.5 6.908V6.75a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3v.158l9.714 5.978a1.5 1.5 0 0 0 1.572 0L22.5 6.908Z" />
</svg>
</div>
<span class="whitespace-nowrap text-xs text-on-surface dark:text-on-surface-dark">Email</span>
</a>
</div>
<!-- Copy -->
<div class="relative px-2">
<label for="shareLink" class="sr-only">share link</label>
<input id="shareLink" type="text" class="w-full bg-surface-alt border border-outline rounded-radius px-2.5 py-2 pr-10 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark" x-ref="shareUrl" x-bind:value="window.location.href" />
<button class="absolute right-5 top-1/2 -translate-y-1/2 rounded-full p-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:outline-offset-0 dark:focus-visible:outline-primary-dark" x-on:click="copyUrlToClipboard($refs.shareUrl.value)" x-on:click.away="copiedToClipboard = false">
<span class="sr-only" x-text="copiedToClipboard ? 'copied' : 'copy the url to clipboard'"></span>
<svg x-cloak x-show="!copiedToClipboard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="1.5" class="size-5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
</svg>
<svg x-cloak x-show="copiedToClipboard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="1.5" aria-hidden="true" class="size-5 stroke-success">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75" />
</svg>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<div x-data class="flex w-full flex-col overflow-hidden border-outline bg-surface-alt text-on-surface has-[p:focus]:outline-2 has-[p:focus]:outline-offset-2 has-[p:focus]:outline-primary dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark dark:has-[p:focus]:outline-primary-dark rounded-radius border">
<div class="p-2">
<p id="promptLabel" class="pb-1 pl-2 text-sm font-bold text-on-surface opacity-60 dark:text-on-surface-dark">Prompt</p>
<p class="scroll-on max-h-44 w-full overflow-y-auto px-2 py-1 focus:outline-hidden" role="textbox" aria-labelledby="promptLabel" x-on:paste.prevent="document.execCommand('insertText', false, $event.clipboardData.getData('text/plain'))" x-ref="promptTextInput" contenteditable></p>
<textarea name="promptText" x-ref="promptText" hidden></textarea>
</div>
<div class="flex w-full items-center px-2.5 py-2">
<button type="button" class="ml-auto flex items-center gap-2 whitespace-nowrap bg-primary px-4 py-2 text-center text-xs font-medium tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark rounded-radius" x-on:click="$refs.promptText.value = $refs.promptTextInput.innerText">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-3" aria-hidden="true">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
Generate
</button>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<div class="relative w-full">
<label for="aiPromt" for="aiPromt" class="sr-only">ai prompt</label>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" aria-hidden="true" class="absolute left-3 top-1/2 size-4 -translate-y-1/2 fill-primary dark:fill-primary-dark">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd" />
</svg>
<input id="aiPromt" type="text" class="w-full border-outline bg-surface-alt border border-outline rounded-radius px-2 py-2 pl-10 pr-24 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark" name="prompt" placeholder="Ask AI ..." />
<button type="button" class="absolute right-3 top-1/2 -translate-y-1/2 bg-primary rounded-radius px-2 py-1 text-xs tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 dark:bg-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark">Generate</button>
</div>

View File

@@ -0,0 +1,36 @@
<div x-data class="flex w-full flex-col overflow-hidden border-outline bg-surface-alt text-on-surface has-[p:focus]:outline-2 has-[p:focus]:outline-offset-2 has-[p:focus]:outline-primary dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark dark:has-[p:focus]:outline-primary-dark rounded-radius border">
<div class="p-2">
<p id="promptLabel" class="pb-1 pl-2 text-sm font-bold text-on-surface opacity-60 dark:text-on-surface-dark">Prompt</p>
<p class="scroll-on max-h-44 w-full overflow-y-auto px-2 py-1 focus:outline-hidden" role="textbox" aria-labelledby="promptLabel" x-on:paste.prevent="document.execCommand('insertText', false, $event.clipboardData.getData('text/plain'))" x-ref="promptTextInput" contenteditable></p>
<textarea name="promptText" x-ref="promptText" hidden></textarea>
</div>
<div class="flex w-full items-center justify-end gap-4 px-2.5 py-2">
<div class="flex items-center gap-2">
<button class="rounded-radius p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Use Camera" aria-label="Use Camera">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<path fill-rule="evenodd" d="M1 8a2 2 0 0 1 2-2h.93a2 2 0 0 0 1.664-.89l.812-1.22A2 2 0 0 1 8.07 3h3.86a2 2 0 0 1 1.664.89l.812 1.22A2 2 0 0 0 16.07 6H17a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8Zm13.5 3a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM10 14a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" clip-rule="evenodd" />
</svg>
</button>
<button class="rounded-radius p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Upload Image" aria-label="Upload Image">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5" aria-hidden="true">
<path fill-rule="evenodd" d="M1 5.25A2.25 2.25 0 0 1 3.25 3h13.5A2.25 2.25 0 0 1 19 5.25v9.5A2.25 2.25 0 0 1 16.75 17H3.25A2.25 2.25 0 0 1 1 14.75v-9.5Zm1.5 5.81v3.69c0 .414.336.75.75.75h13.5a.75.75 0 0 0 .75-.75v-2.69l-2.22-2.219a.75.75 0 0 0-1.06 0l-1.91 1.909.47.47a.75.75 0 1 1-1.06 1.06L6.53 8.091a.75.75 0 0 0-1.06 0l-2.97 2.97ZM12 7a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" clip-rule="evenodd" />
</svg>
</button>
<button class="rounded-radius p-1 text-on-surface/75 hover:bg-surface-dark/10 hover:text-on-surface focus:outline-hidden focus-visible:text-on-surface focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-primary active:bg-surface-dark/5 active:-outline-offset-2 dark:text-on-surface-dark/75 dark:hover:bg-surface/10 dark:hover:text-on-surface-dark dark:focus-visible:text-on-surface-dark dark:focus-visible:outline-primary-dark dark:active:bg-surface/5" title="Use Voice" aria-label="Use Voice">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5" aria-hidden="true">
<path d="M7 4a3 3 0 0 1 6 0v6a3 3 0 1 1-6 0V4Z" />
<path d="M5.5 9.643a.75.75 0 0 0-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5h-1.5v-1.546A6.001 6.001 0 0 0 16 10v-.357a.75.75 0 0 0-1.5 0V10a4.5 4.5 0 0 1-9 0v-.357Z" />
</svg>
</button>
</div>
<button type="button" class="flex items-center gap-2 whitespace-nowrap bg-primary px-4 py-2 text-center text-xs font-medium tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark rounded-radius" x-on:click="$refs.promptText.value = $refs.promptTextInput.innerText">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-3" aria-hidden="true">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
Generate
</button>
</div>
</div>

View File

@@ -0,0 +1,19 @@
<div class="flex w-full flex-col gap-2">
<div class="bg-surface-alt border-outline py-4 dark:border-outline-dark dark:bg-surface-dark-alt rounded-radius border">
<ul class="text-on-surface dark:text-on-surface-dark text-sm">
<li class="px-4 py-1 hover:bg-surface-dark/5 dark:hover:bg-surface/5"><button class="text-left">a penguin swimming in the ocean</button></li>
<li class="px-4 py-1 hover:bg-surface-dark/5 dark:hover:bg-surface/5"><button class="text-left">a penguin fishing under the water</button></li>
<li class="px-4 py-1 hover:bg-surface-dark/5 dark:hover:bg-surface/5"><button class="text-left">a penguin walking slowly on the snow</button></li>
<li class="px-4 py-1 hover:bg-surface-dark/5 dark:hover:bg-surface/5"><button class="text-left">a penguin hugging another penguin</button></li>
</ul>
</div>
<div class="relative w-full">
<label for="aiPromt" for="aiPromt" class="sr-only">ai prompt</label>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" aria-hidden="true" class="absolute left-3 top-1/2 size-4 -translate-y-1/2 fill-primary dark:fill-primary-dark">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd" />
</svg>
<input id="aiPromt" type="text" class="w-full border-outline bg-surface-alt border border-outline rounded-radius px-2 py-2 pl-10 pr-24 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark" value="a penguin" name="prompt" placeholder="Ask AI ..." />
<button type="button" class="absolute right-3 top-1/2 -translate-y-1/2 bg-primary rounded-radius px-2 py-1 text-xs tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 dark:bg-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark">Generate</button>
</div>
</div>

View File

@@ -0,0 +1,74 @@
<!-- Create - primary -->
<button type="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm border px-4 py-2 text-sm font-medium tracking-wide bg-primary border-primary text-on-primary transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-primary-dark dark:border-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
Create
</button>
<!-- Create - secondary -->
<button type="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm border px-4 py-2 text-sm font-medium tracking-wide bg-secondary border-secondary text-on-secondary transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-secondary-dark dark:border-secondary-dark dark:text-on-secondary-dark dark:focus-visible:outline-secondary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
Create
</button>
<!-- Create - inverse -->
<button type="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm border px-4 py-2 text-sm font-medium tracking-wide bg-surface-dark border-surface-dark text-on-surface-dark transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface-dark active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-surface dark:border-surface dark:text-on-surface dark:focus-visible:outline-surface">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
Create
</button>
<!-- Generate - primary -->
<button type="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm border px-4 py-2 text-sm font-medium tracking-wide bg-primary border-primary text-on-primary transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-primary-dark dark:border-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M9.5 2.672a.5.5 0 1 0 1 0V.843a.5.5 0 0 0-1 0zm4.5.035A.5.5 0 0 0 13.293 2L12 3.293a.5.5 0 1 0 .707.707zM7.293 4A.5.5 0 1 0 8 3.293L6.707 2A.5.5 0 0 0 6 2.707zm-.621 2.5a.5.5 0 1 0 0-1H4.843a.5.5 0 1 0 0 1zm8.485 0a.5.5 0 1 0 0-1h-1.829a.5.5 0 0 0 0 1zM13.293 10A.5.5 0 1 0 14 9.293L12.707 8a.5.5 0 1 0-.707.707zM9.5 11.157a.5.5 0 0 0 1 0V9.328a.5.5 0 0 0-1 0zm1.854-5.097a.5.5 0 0 0 0-.706l-.708-.708a.5.5 0 0 0-.707 0L8.646 5.94a.5.5 0 0 0 0 .707l.708.708a.5.5 0 0 0 .707 0l1.293-1.293Zm-3 3a.5.5 0 0 0 0-.706l-.708-.708a.5.5 0 0 0-.707 0L.646 13.94a.5.5 0 0 0 0 .707l.708.708a.5.5 0 0 0 .707 0z"/>
</svg>
Generate
</button>
<!-- Generate - secondary -->
<button type="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm border px-4 py-2 text-sm font-medium tracking-wide bg-secondary border-secondary text-on-secondary transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-secondary-dark dark:border-secondary-dark dark:text-on-secondary-dark dark:focus-visible:outline-secondary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M9.5 2.672a.5.5 0 1 0 1 0V.843a.5.5 0 0 0-1 0zm4.5.035A.5.5 0 0 0 13.293 2L12 3.293a.5.5 0 1 0 .707.707zM7.293 4A.5.5 0 1 0 8 3.293L6.707 2A.5.5 0 0 0 6 2.707zm-.621 2.5a.5.5 0 1 0 0-1H4.843a.5.5 0 1 0 0 1zm8.485 0a.5.5 0 1 0 0-1h-1.829a.5.5 0 0 0 0 1zM13.293 10A.5.5 0 1 0 14 9.293L12.707 8a.5.5 0 1 0-.707.707zM9.5 11.157a.5.5 0 0 0 1 0V9.328a.5.5 0 0 0-1 0zm1.854-5.097a.5.5 0 0 0 0-.706l-.708-.708a.5.5 0 0 0-.707 0L8.646 5.94a.5.5 0 0 0 0 .707l.708.708a.5.5 0 0 0 .707 0l1.293-1.293Zm-3 3a.5.5 0 0 0 0-.706l-.708-.708a.5.5 0 0 0-.707 0L.646 13.94a.5.5 0 0 0 0 .707l.708.708a.5.5 0 0 0 .707 0z"/>
</svg>
Generate
</button>
<!-- Generate - inverse -->
<button type="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm border px-4 py-2 text-sm font-medium tracking-wide bg-surface-dark border-surface-dark text-on-surface-dark transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface-dark active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-surface dark:border-surface dark:text-on-surface dark:focus-visible:outline-surface">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path d="M9.5 2.672a.5.5 0 1 0 1 0V.843a.5.5 0 0 0-1 0zm4.5.035A.5.5 0 0 0 13.293 2L12 3.293a.5.5 0 1 0 .707.707zM7.293 4A.5.5 0 1 0 8 3.293L6.707 2A.5.5 0 0 0 6 2.707zm-.621 2.5a.5.5 0 1 0 0-1H4.843a.5.5 0 1 0 0 1zm8.485 0a.5.5 0 1 0 0-1h-1.829a.5.5 0 0 0 0 1zM13.293 10A.5.5 0 1 0 14 9.293L12.707 8a.5.5 0 1 0-.707.707zM9.5 11.157a.5.5 0 0 0 1 0V9.328a.5.5 0 0 0-1 0zm1.854-5.097a.5.5 0 0 0 0-.706l-.708-.708a.5.5 0 0 0-.707 0L8.646 5.94a.5.5 0 0 0 0 .707l.708.708a.5.5 0 0 0 .707 0l1.293-1.293Zm-3 3a.5.5 0 0 0 0-.706l-.708-.708a.5.5 0 0 0-.707 0L.646 13.94a.5.5 0 0 0 0 .707l.708.708a.5.5 0 0 0 .707 0z"/>
</svg>
Generate
</button>
<!-- FAB - primary -->
<button aria-label="AI Agent" class="inline-flex items-center justify-center rounded-full border p-4 tracking-wide bg-primary border-primary text-on-primary transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-primary-dark dark:border-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-6" aria-hidden="true">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
</button>
<!-- FAB - secondary -->
<button aria-label="AI Agent" class="inline-flex items-center justify-center rounded-full border p-4 tracking-wide bg-secondary border-secondary text-on-secondary transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-secondary-dark dark:border-secondary-dark dark:text-on-secondary-dark dark:focus-visible:outline-secondary-dark">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-6" aria-hidden="true">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
</button>
<!-- FAB - inverse -->
<button aria-label="AI Agent" class="inline-flex items-center justify-center rounded-full border p-4 tracking-wide bg-surface-dark border-surface-dark text-on-surface-dark transition hover:opacity-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface-dark active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-surface dark:border-surface dark:text-on-surface dark:focus-visible:outline-surface">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-6" aria-hidden="true">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
</svg>
</button>

View File

@@ -0,0 +1,48 @@
<!-- cyan-blue -->
<button type="button" class="active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 font-medium bg-linear-to-r from-cyan-600 to-blue-600 focus-visible:outline-cyan-600 dark:focus-visible:outline-cyan-600 hover:opacity-75 inline-flex items-center justify-center gap-2 px-4 py-2 rounded-radius text-white text-sm tracking-wide transition whitespace-nowrap">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
AI Assist
</button>
<!-- blue-purple -->
<button type="button" class="active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 font-medium bg-linear-to-br from-blue-600 to-purple-600 focus-visible:outline-blue-600 dark:focus-visible:outline-blue-600 hover:opacity-75 inline-flex items-center justify-center gap-2 px-4 py-2 rounded-radius text-white text-sm tracking-wide transition whitespace-nowrap">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
AI Assist
</button>
<!-- violet -->
<button type="button" class="active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 font-medium bg-linear-to-b from-violet-800 to-violet-500 focus-visible:outline-violet-800 dark:focus-visible:outline-violet-800 hover:opacity-75 inline-flex items-center justify-center gap-2 px-4 py-2 rounded-radius text-white text-sm tracking-wide transition whitespace-nowrap">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
AI Assist
</button>
<!-- purple-pink -->
<button type="button" class="active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 font-medium bg-linear-to-r from-purple-600 to-pink-600 focus-visible:outline-purple-600 dark:focus-visible:outline-purple-600 hover:opacity-75 inline-flex items-center justify-center gap-2 px-4 py-2 rounded-radius text-white text-sm tracking-wide transition whitespace-nowrap">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
AI Assist
</button>
<!-- indigo-blue -->
<button type="button" class="active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 font-medium bg-linear-to-r from-indigo-600 to-blue-600 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-600 hover:opacity-75 inline-flex items-center justify-center gap-2 px-4 py-2 rounded-radius text-white text-sm tracking-wide transition whitespace-nowrap">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
AI Assist
</button>
<!-- green-blue-indigo -->
<button type="button" class="active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 font-medium bg-linear-to-br from-green-500 via-blue-500 to-indigo-700 focus-visible:outline-indigo-500 dark:focus-visible:outline-indigo-500 hover:opacity-75 inline-flex items-center justify-center gap-2 px-4 py-2 rounded-radius text-white text-sm tracking-wide transition whitespace-nowrap">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-4">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd"/>
</svg>
AI Assist
</button>

View File

@@ -0,0 +1,10 @@
<div x-data="{ aiPromptIsVisible: false }" class="fixed left-1/2 -translate-x-1/2 bottom-5 z-10 w-full max-w-xl" x-on:click.away="aiPromptIsVisible = false" x-on:keydown.cmd.shift.a.window.prevent="aiPromptIsVisible = ! aiPromptIsVisible" x-on:keydown.ctrl.shift.a.window.prevent="aiPromptIsVisible = ! aiPromptIsVisible" x-on:keydown.esc.window.prevent="aiPromptIsVisible = false">
<div x-show="aiPromptIsVisible" class="relative w-full" x-transition x-trap="aiPromptIsVisible">
<label for="aiPromt" for="aiPromt" class="sr-only">ai prompt</label>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" aria-hidden="true" class="absolute left-3 top-1/2 size-4 -translate-y-1/2 fill-primary dark:fill-primary-dark">
<path fill-rule="evenodd" d="M5 4a.75.75 0 0 1 .738.616l.252 1.388A1.25 1.25 0 0 0 6.996 7.01l1.388.252a.75.75 0 0 1 0 1.476l-1.388.252A1.25 1.25 0 0 0 5.99 9.996l-.252 1.388a.75.75 0 0 1-1.476 0L4.01 9.996A1.25 1.25 0 0 0 3.004 8.99l-1.388-.252a.75.75 0 0 1 0-1.476l1.388-.252A1.25 1.25 0 0 0 4.01 6.004l.252-1.388A.75.75 0 0 1 5 4ZM12 1a.75.75 0 0 1 .721.544l.195.682c.118.415.443.74.858.858l.682.195a.75.75 0 0 1 0 1.442l-.682.195a1.25 1.25 0 0 0-.858.858l-.195.682a.75.75 0 0 1-1.442 0l-.195-.682a1.25 1.25 0 0 0-.858-.858l-.682-.195a.75.75 0 0 1 0-1.442l.682-.195a1.25 1.25 0 0 0 .858-.858l.195-.682A.75.75 0 0 1 12 1ZM10 11a.75.75 0 0 1 .728.568.968.968 0 0 0 .704.704.75.75 0 0 1 0 1.456.968.968 0 0 0-.704.704.75.75 0 0 1-1.456 0 .968.968 0 0 0-.704-.704.75.75 0 0 1 0-1.456.968.968 0 0 0 .704-.704A.75.75 0 0 1 10 11Z" clip-rule="evenodd" />
</svg>
<input id="aiPromt" type="text" class="w-full border-outline bg-surface-alt border border-outline rounded-radius px-2 py-2 pl-10 pr-24 text-sm text-on-surface focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark" name="prompt" placeholder="Ask AI to generate ..." />
<button type="button" class="absolute right-3 top-1/2 -translate-y-1/2 bg-primary rounded-radius px-2 py-1 text-xs tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 dark:bg-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark">Generate</button>
</div>
</div>

View File

@@ -0,0 +1,44 @@
<button type="button" aria-label="AI Agent" class="rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:outline-offset-0 dark:focus-visible:outline-primary-dark">
<svg class="w-16" viewBox="0 0 838 837" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<path d="M515.718 110.605C621.291 149.011 714.28 313.637 646.577 499.743C578.874 685.848 438.407 805.582 332.834 767.176C227.261 728.77 172.327 427.631 240.03 241.525C264.94 173.052 302.98 129.691 346.376 104.145C420.926 60.2597 448.989 86.3295 515.718 110.605Z" fill="url(#linearGradient1)" fill-opacity="0.6" />
<path d="M156.136 453.009C156.169 480.494 140.036 537.977 222.056 604.127C338.957 655.053 526.064 778.991 591.853 627.973C657.641 476.956 715.723 241.089 598.822 190.163C481.921 139.237 221.924 301.992 156.136 453.009Z" fill="url(#linearGradient2)" fill-opacity="0.6" />
<path opacity="0.8" d="M766.324 448.634C743.549 558.643 594.059 674.407 400.133 634.258C206.208 594.109 67.4634 472.382 90.2388 362.373C113.014 252.364 403.08 154.565 597.005 194.714C668.356 209.486 716.75 240.874 748.289 280.13C802.469 347.57 780.72 379.1 766.324 448.634Z" fill="url(#linearGradient3)" fill-opacity="0.5" />
<ellipse cx="419" cy="409" rx="419" ry="409" fill="url(#radialGradient1)" />
<ellipse class="animate-pulse" cx="419" cy="409" rx="419" ry="409" fill="url(#radialGradient2)" />
<ellipse cx="419" cy="409" rx="419" ry="409" fill="url(#radialGradient3)" />
<defs>
<lineargradient id="linearGradient1" x1="420.705" y1="249.747" x2="423.671" y2="663.385" gradientUnits="userSpaceOnUse" >
<stop stop-color="#00D1FF" />
<stop offset="1" stop-color="#C626FF" stop-opacity="0" />
</lineargradient>
<lineargradient id="linearGradient2" x1="487.879" y1="-248.502" y2="140.966" gradientUnits="userSpaceOnUse">
<stop stop-color="#00A3FF" stop-opacity="0.14" />
<stop offset="1" stop-color="#FF00B8" />
</lineargradient>
<lineargradient id="linearGradient3" x1="161.766" y1="376.102" x2="594.449" y2="567.328" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE0"/>
<stop offset="1" stop-color="#C626FF" stop-opacity="0" />
</lineargradient>
<radialgradient id="radialGradient1" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(419 409) rotate(90) scale(358.5 367.131)">
<stop stop-color="white" />
<stop offset="0.193741" stop-color="#E4E4E4" />
<stop offset="1" stop-color="#737373" stop-opacity="0" />
</radialgradient>
<radialgradient id="radialGradient2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(419 409) rotate(90) scale(293.5 300.566)">
<stop stop-color="white" />
<stop offset="0.314072" stop-color="white" />
<stop offset="1" stop-color="#737373" stop-opacity="0" />
</radialgradient>
<radialgradient id="radialGradient3" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(419 409) rotate(90) scale(534 546.857)">
<stop stop-color="#545454" stop-opacity="0" />
<stop offset="1" stop-color="#5D64FF" stop-opacity="0.35" />
</radialgradient>
</defs>
</svg>
</button>

View File

@@ -0,0 +1,109 @@
<!-- info Alert -->
<div x-data="{ alertIsVisible: true }" x-show="alertIsVisible"
class="relative w-full overflow-hidden rounded-radius border border-info bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-90">
<div class="flex w-full items-center gap-2 bg-info/10 p-4">
<div class="bg-info/15 text-info rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-semibold text-info">Update Available</h3>
<p class="text-xs font-medium sm:text-sm">A new version is available. Please update to the latest version.
</p>
</div>
<button type="button" @click="alertIsVisible = false" class="ml-auto" aria-label="dismiss alert">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor"
fill="none" stroke-width="2.5" class="w-4 h-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- success Alert -->
<div x-data="{ alertIsVisible: true }" x-show="alertIsVisible"
class="relative w-full overflow-hidden rounded-radius border border-success bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-90">
<div class="flex w-full items-center gap-2 bg-success/10 p-4">
<div class="bg-success/15 text-success rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-semibold text-success">Successfully Subscribed</h3>
<p class="text-xs font-medium sm:text-sm">Success! You've subscribed to our newsletter. Welcome aboard!</p>
</div>
<button type="button" @click="alertIsVisible = false" class="ml-auto" aria-label="dismiss alert">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor"
fill="none" stroke-width="2.5" class="w-4 h-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- warning Alert -->
<div x-data="{ alertIsVisible: true }" x-show="alertIsVisible"
class="relative w-full overflow-hidden rounded-radius border border-warning bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-90">
<div class="flex w-full items-center gap-2 bg-warning/10 p-4">
<div class="bg-warning/15 text-warning rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-semibold text-warning">Credit Card Expires Soon</h3>
<p class="text-xs font-medium sm:text-sm">Your credit card expires soon. Please update your payment
information.</p>
</div>
<button type="button" @click="alertIsVisible = false" class="ml-auto" aria-label="dismiss alert">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor"
fill="none" stroke-width="2.5" class="w-4 h-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- danger Alert -->
<div x-data="{ alertIsVisible: true }" x-show="alertIsVisible"
class="relative w-full overflow-hidden rounded-radius border border-danger bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-90">
<div class="flex w-full items-center gap-2 bg-danger/10 p-4">
<div class="bg-danger/15 text-danger rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-semibold text-danger">Invalid Email Address</h3>
<p class="text-xs font-medium sm:text-sm">The email address you entered is invalid. Please try again.</p>
</div>
<button type="button" @click="alertIsVisible = false" class="ml-auto" aria-label="dismiss alert">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor"
fill="none" stroke-width="2.5" class="w-4 h-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>

View File

@@ -0,0 +1,127 @@
<!-- info Alert -->
<div class="relative w-full overflow-hidden rounded-radius border border-info bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert">
<div class="flex w-full items-center gap-2 bg-info/10 p-4">
<div class="bg-info/15 text-info rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex flex-col gap-2 ml-2">
<div>
<h3 class="text-sm font-semibold text-info">Update Available</h3>
<p class="text-xs font-medium sm:text-sm">A new version is available. Please update to the latest
version.</p>
</div>
<div class="flex items-center gap-4">
<button type="button"
class="whitespace-nowrap text-center text-sm font-semibold tracking-wide text-info transition hover:opacity-75 active:opacity-100">
Update Now
</button>
<button type="button"
class="whitespace-nowrap text-center text-sm font-medium tracking-wide text-on-surface transition hover:opacity-75 dark:text-on-surface-dark active:opacity-100">
Dismiss
</button>
</div>
</div>
</div>
</div>
<!-- success Alert -->
<div class="relative w-full overflow-hidden rounded-radius border border-success bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert">
<div class="flex w-full items-center gap-2 bg-success/10 p-4">
<div class="bg-success/15 text-success rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex flex-col gap-2 ml-2">
<div>
<h3 class="text-sm font-semibold text-success">Successfully Subscribed</h3>
<p class="text-xs font-medium sm:text-sm">Success! You've subscribed to our newsletter. Welcome aboard!
</p>
</div>
<div class="flex items-center gap-4">
<button type="button"
class="whitespace-nowrap text-center text-sm font-semibold tracking-wide text-success transition hover:opacity-75 active:opacity-100">
Dashboard
</button>
<button type="button"
class="whitespace-nowrap text-center text-sm font-medium tracking-wide text-on-surface transition hover:opacity-75 dark:text-on-surface-dark active:opacity-100">
Dismiss
</button>
</div>
</div>
</div>
</div>
<!-- warning Alert -->
<div class="relative w-full overflow-hidden rounded-radius border border-warning bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert">
<div class="flex w-full items-center gap-2 bg-warning/10 p-4">
<div class="bg-warning/15 text-warning rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex flex-col gap-2 ml-2">
<div>
<h3 class="text-sm font-semibold text-warning">Credit Card Expires Soon</h3>
<p class="text-xs font-medium sm:text-sm">Your credit card expires soon. Please update your payment
information.</p>
</div>
<div class="flex items-center gap-4">
<button type="button"
class="whitespace-nowrap text-center text-sm font-semibold tracking-wide text-warning transition hover:opacity-75 active:opacity-100">
Update Now
</button>
<button type="button"
class="whitespace-nowrap text-center text-sm font-medium tracking-wide text-on-surface transition hover:opacity-75 dark:text-on-surface-dark active:opacity-100">
Dismiss
</button>
</div>
</div>
</div>
</div>
<!-- danger Alert -->
<div class="relative w-full overflow-hidden rounded-radius border border-danger bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert">
<div class="flex w-full items-center gap-2 bg-danger/10 p-4">
<div class="bg-danger/15 text-danger rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex flex-col gap-2 ml-2">
<div>
<h3 class="text-sm font-semibold text-danger">Invalid Email Address</h3>
<p class="text-xs font-medium sm:text-sm">The email address you entered is invalid. Please try again.
</p>
</div>
<div class="flex items-center gap-4">
<button type="button"
class="whitespace-nowrap text-center text-sm font-semibold tracking-wide text-danger transition hover:opacity-75 active:opacity-100">
Try Again
</button>
<button type="button"
class="whitespace-nowrap text-center text-sm font-medium tracking-wide text-on-surface transition hover:opacity-75 dark:text-on-surface-dark active:opacity-100">
Dismiss
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,103 @@
<!-- info Alert -->
<div class="relative w-full overflow-hidden rounded-radius border border-sky-500 bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert">
<div class="flex w-full items-center gap-2 bg-info/10 p-4">
<div class="bg-sky-500/15 text-sky-500 rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex w-full flex-col items-center justify-between gap-2 ml-2 sm:flex-row">
<div>
<h3 class="text-sm font-semibold text-info">Update Available</h3>
<p class="text-xs font-medium sm:text-sm">A new version is available. Please update to the latest
version.</p>
</div>
<a href="#"
class="whitespace-nowrap ml-auto text-sm font-medium text-info tracking-wide transition hover:opacity-75 text-center active:opacity-100">
Details
</a>
</div>
</div>
</div>
<!-- success Alert -->
<div class="relative w-full overflow-hidden rounded-radius border border-green-500 bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert">
<div class="flex w-full items-center gap-2 bg-success/10 p-4">
<div class="bg-green-500/15 text-green-500 rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex w-full flex-col items-center justify-between gap-2 ml-2 sm:flex-row">
<div>
<h3 class="text-sm font-semibold text-success">Successfully Subscribed</h3>
<p class="text-xs font-medium sm:text-sm">Success! You've subscribed to our newsletter. Welcome aboard!
</p>
</div>
<a href="#"
class="whitespace-nowrap ml-auto text-sm font-medium text-success tracking-wide transition hover:opacity-75 text-center active:opacity-100">
Details
</a>
</div>
</div>
</div>
<!-- warning Alert -->
<div class="relative w-full overflow-hidden rounded-radius border border-amber-500 bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert">
<div class="flex w-full items-center gap-2 bg-warning/10 p-4">
<div class="bg-amber-500/15 text-amber-500 rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex w-full flex-col items-center justify-between gap-2 ml-2 sm:flex-row">
<div>
<h3 class="text-sm font-semibold text-warning">Credit Card Expires Soon</h3>
<p class="text-xs font-medium sm:text-sm">Your credit card expires soon. Please update your payment
information.</p>
</div>
<a href="#"
class="whitespace-nowrap ml-auto text-sm font-medium text-warning tracking-wide transition hover:opacity-75 text-center active:opacity-100">
Details
</a>
</div>
</div>
</div>
<!-- danger Alert -->
<div class="relative w-full overflow-hidden rounded-radius border border-red-500 bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert">
<div class="flex w-full items-center gap-2 bg-danger/10 p-4">
<div class="bg-red-500/15 text-red-500 rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex w-full flex-col items-center justify-between gap-2 ml-2 sm:flex-row">
<div>
<h3 class="text-sm font-semibold text-danger">Invalid Email Address</h3>
<p class="text-xs font-medium sm:text-sm">The email address you entered is invalid. Please try again.
</p>
</div>
<a href="#"
class="whitespace-nowrap ml-auto text-sm font-medium text-danger tracking-wide transition hover:opacity-75 text-center active:opacity-100">
Details
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,48 @@
<!-- danger Alert -->
<div class="relative w-full overflow-hidden rounded-radius border border-danger bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert">
<div class="flex w-full items-center gap-2 bg-danger/10 p-4">
<div class="bg-danger/15 text-danger rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-semibold text-danger">Password is not strong</h3>
<p class="text-xs font-medium sm:text-sm">The password you entered does not meet the requirements. Make sure
your password:</p>
<ul class="mt-2 list-inside list-disc pl-2 text-xs font-medium text-danger sm:text-sm">
<li>has <strong>minimum 8</strong> characters</li>
<li>includes <strong>both upper and lower </strong> cases</li>
<li>contains <strong>at least one number</strong></li>
</ul>
</div>
</div>
</div>
<!-- info Alert -->
<div class="relative w-full overflow-hidden rounded-radius border border-info bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark"
role="alert">
<div class="flex w-full items-center gap-2 bg-info/10 p-4">
<div class="bg-info/15 text-info rounded-full p-1" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6"
aria-hidden="true">
<path fill-rule="evenodd"
d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM9 9a.75.75 0 0 0 0 1.5h.253a.25.25 0 0 1 .244.304l-.459 2.066A1.75 1.75 0 0 0 10.747 15H11a.75.75 0 0 0 0-1.5h-.253a.25.25 0 0 1-.244-.304l.459-2.066A1.75 1.75 0 0 0 9.253 9H9Z"
clip-rule="evenodd" />
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-semibold text-info">Password Requirements</h3>
<p class="text-xs font-medium sm:text-sm">In order to keep your account secure, make sure your password:</p>
<ul class="mt-2 list-inside list-disc pl-2 text-xs font-medium sm:text-sm">
<li>has <strong>minimum 8</strong> characters</li>
<li>includes <strong>both upper and lower </strong> cases</li>
<li>contains <strong>at least one number</strong></li>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<!-- info Border Avatar -->
<img class="size-14 rounded-full border-2 border-info object-cover object-center p-0.5"
src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">
<!-- success Border Avatar -->
<img class="size-14 rounded-full border-2 border-success object-cover object-center p-0.5"
src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">
<!-- warning Border Avatar -->
<img class="size-14 rounded-full border-2 border-warning object-cover object-center p-0.5"
src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">
<!-- danger Border Avatar -->
<img class="size-14 rounded-full border-2 border-danger object-cover object-center p-0.5"
src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">

View File

@@ -0,0 +1,56 @@
<!-- default Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-outline bg-surface-alt text-on-surface/50 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-full h-full mt-3">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
</svg>
</span>
<!-- inverse Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-outline-dark bg-surface-dark-alt text-on-surface-dark/50 dark:border-outline dark:bg-surface-alt dark:text-on-surface/50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-full h-full mt-3">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
</svg>
</span>
<!-- primary Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-primary bg-primary text-on-primary/50 dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary/50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-full h-full mt-3">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
</svg>
</span>
<!-- secondary Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-secondary bg-secondary text-on-secondary/50 dark:border-secondary-dark dark:bg-secondary-dark dark:text-on-secondary-dark/50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-full h-full mt-3">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
</svg>
</span>
<!-- info Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-info bg-info text-on-info/50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-full h-full mt-3">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
</svg>
</span>
<!-- success Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-success bg-success text-on-success/50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-full h-full mt-3">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
</svg>
</span>
<!-- warning Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-warning bg-warning text-on-warning/50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-full h-full mt-3">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
</svg>
</span>
<!-- danger Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-danger bg-danger text-on-danger/50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-full h-full mt-3">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
</svg>
</span>

View File

@@ -0,0 +1,24 @@
<!-- default Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-outline bg-surface-alt text-2xl font-bold tracking-wider text-on-surface/80 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/80">JS</span>
<!-- inverse Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-outline-dark bg-surface-dark-alt text-2xl font-bold tracking-wider text-on-surface-dark/80 dark:border-outline dark:bg-surface-alt dark:text-on-surface/80">JS</span>
<!-- primary Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-primary bg-primary text-2xl font-bold tracking-wider text-on-primary/80 dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary/80">JS</span>
<!-- secondary Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-secondary bg-secondary text-2xl font-bold tracking-wider text-on-secondary/80 dark:border-secondary-dark dark:bg-secondary-dark dark:text-on-secondary-dark/80">JS</span>
<!-- info Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-info bg-info text-2xl font-bold tracking-wider text-on-info/80">JS</span>
<!-- success Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-success bg-success text-2xl font-bold tracking-wider text-on-success/80">JS</span>
<!-- warning Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-warning bg-warning text-2xl font-bold tracking-wider text-on-warning/80">JS</span>
<!-- danger Avatar -->
<span class="flex size-14 items-center justify-center overflow-hidden rounded-full border border-danger bg-danger text-2xl font-bold tracking-wider text-on-danger/80">JS</span>

View File

@@ -0,0 +1,35 @@
<!-- Avatar - offline Status -->
<div class="relative w-fit">
<img class="size-14 rounded-full object-cover object-center" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">
<span class="absolute size-4 bottom-0.5 end-0 rounded-full border-2 border-surface dark:border-surface-dark bg-outline dark:bg-outline-dark">
</span>
</div>
<!-- Avatar - info Status -->
<div class="relative w-fit">
<img class="size-14 rounded-full object-cover object-center" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">
<span class="absolute size-4 bottom-0.5 end-0 rounded-full border-2 border-surface dark:border-surface-dark bg-info">
</span>
</div>
<!-- Avatar - success Status -->
<div class="relative w-fit">
<img class="size-14 rounded-full object-cover object-center" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">
<span class="absolute size-4 bottom-0.5 end-0 rounded-full border-2 border-surface dark:border-surface-dark bg-success">
</span>
</div>
<!-- Avatar - warning Status -->
<div class="relative w-fit">
<img class="size-14 rounded-full object-cover object-center" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">
<span class="absolute size-4 bottom-0.5 end-0 rounded-full border-2 border-surface dark:border-surface-dark bg-warning">
</span>
</div>
<!-- Avatar - danger Status -->
<div class="relative w-fit">
<img class="size-14 rounded-full object-cover object-center" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">
<span class="absolute size-4 bottom-0.5 end-0 rounded-full border-2 border-surface dark:border-surface-dark bg-danger">
</span>
</div>

View File

@@ -0,0 +1 @@
<img class="size-14 rounded-full object-cover" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">

View File

@@ -0,0 +1 @@
<img class="size-14 rounded-md object-cover" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">

View File

@@ -0,0 +1,6 @@
<div class="flex items-center -space-x-4">
<img class="size-14 rounded-full border-2 border-white object-cover dark:border-neutral-950" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-8.webp" alt="Rounded avatar">
<img class="size-14 rounded-full border-2 border-white object-cover dark:border-neutral-950" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-3.webp" alt="Rounded avatar">
<img class="size-14 rounded-full border-2 border-white object-cover dark:border-neutral-950" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-5.webp" alt="Rounded avatar">
<img class="size-14 rounded-full border-2 border-white object-cover dark:border-neutral-950" src="https://penguinui.s3.amazonaws.com/component-assets/avatar-1.webp" alt="Rounded avatar">
</div>

View File

@@ -0,0 +1,36 @@
<!-- primary Badge -->
<span class="flex size-3 items-center justify-center rounded-full bg-primary dark:bg-primary-dark" aria-label="notification">
<span class="size-3 animate-ping rounded-full bg-primary motion-reduce:animate-none dark:bg-primary-dark">
</span>
</span>
<!-- secondary Badge -->
<span class="flex size-3 items-center justify-center rounded-full bg-secondary dark:bg-secondary-dark" aria-label="notification">
<span class="size-3 animate-ping rounded-full bg-secondary motion-reduce:animate-none dark:bg-secondary-dark">
</span>
</span>
<!-- info Badge -->
<span class="flex size-3 items-center justify-center rounded-full bg-info dark:bg-info" aria-label="notification">
<span class="size-3 animate-ping rounded-full bg-info motion-reduce:animate-none dark:bg-info">
</span>
</span>
<!-- success Badge -->
<span class="flex size-3 items-center justify-center rounded-full bg-success dark:bg-success" aria-label="notification">
<span class="size-3 animate-ping rounded-full bg-success motion-reduce:animate-none dark:bg-success">
</span>
</span>
<!-- warning Badge -->
<span class="flex size-3 items-center justify-center rounded-full bg-warning dark:bg-warning" aria-label="notification">
<span class="size-3 animate-ping rounded-full bg-warning motion-reduce:animate-none dark:bg-warning">
</span>
</span>
<!-- danger Badge -->
<span class="flex size-3 items-center justify-center rounded-full bg-danger dark:bg-danger" aria-label="notification">
<span class="size-3 animate-ping rounded-full bg-danger motion-reduce:animate-none dark:bg-danger">
</span>
</span>

View File

@@ -0,0 +1,60 @@
<!-- primary Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-primary bg-surface text-xs font-medium text-primary dark:border-primary-dark dark:bg-surface-dark dark:text-primary-dark">
<span class="flex items-center gap-1 bg-primary/10 px-2 py-1 dark:bg-primary-dark/10">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-3">
<path fill-rule="evenodd" d="M11.097 1.515a.75.75 0 01.589.882L10.666 7.5h4.47l1.079-5.397a.75.75 0 111.47.294L16.665 7.5h3.585a.75.75 0 010 1.5h-3.885l-1.2 6h3.585a.75.75 0 010 1.5h-3.885l-1.08 5.397a.75.75 0 11-1.47-.294l1.02-5.103h-4.47l-1.08 5.397a.75.75 0 01-1.47-.294l1.02-5.103H3.75a.75.75 0 110-1.5h3.885l1.2-6H5.25a.75.75 0 010-1.5h3.885l1.08-5.397a.75.75 0 01.882-.588zM10.365 9l-1.2 6h4.47l1.2-6h-4.47z" clip-rule="evenodd"/>
</svg>
Penguin
</span>
</span>
<!-- secondary Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-secondary bg-surface text-xs font-medium text-secondary dark:border-secondary-dark dark:bg-surface-dark dark:text-secondary-dark">
<span class="flex items-center gap-1 bg-secondary/10 px-2 py-1 dark:bg-secondary-dark/10">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="1.4" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
Filter
</span>
</span>
<!-- info Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-info bg-surface text-xs font-medium text-info dark:border-info dark:bg-surface-dark dark:text-info">
<span class="flex items-center gap-1 bg-info/10 px-2 py-1 dark:bg-info/10">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-4">
<path fill-rule="evenodd" d="M8.603 3.799A4.49 4.49 0 0112 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 013.498 1.307 4.491 4.491 0 011.307 3.497A4.49 4.49 0 0121.75 12a4.49 4.49 0 01-1.549 3.397 4.491 4.491 0 01-1.307 3.497 4.491 4.491 0 01-3.497 1.307A4.49 4.49 0 0112 21.75a4.49 4.49 0 01-3.397-1.549 4.49 4.49 0 01-3.498-1.306 4.491 4.491 0 01-1.307-3.498A4.49 4.49 0 012.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 011.307-3.497 4.49 4.49 0 013.497-1.307zm7.007 6.387a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clip-rule="evenodd"/>
</svg>
Verified
</span>
</span>
<!-- success Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-success bg-surface text-xs font-medium text-success dark:border-success dark:bg-surface-dark dark:text-success">
<span class="flex items-center gap-1 bg-success/10 px-2 py-1 dark:bg-success/10">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-4">
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clip-rule="evenodd"/>
</svg>
Active
</span>
</span>
<!-- warning Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-warning bg-surface text-xs font-medium text-warning dark:border-warning dark:bg-surface-dark dark:text-warning">
<span class="flex items-center gap-1 bg-warning/10 px-2 py-1 dark:bg-warning/10">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-4">
<path fill-rule="evenodd" d="M11.484 2.17a.75.75 0 011.032 0 11.209 11.209 0 007.877 3.08.75.75 0 01.722.515 12.74 12.74 0 01.635 3.985c0 5.942-4.064 10.933-9.563 12.348a.749.749 0 01-.374 0C6.314 20.683 2.25 15.692 2.25 9.75c0-1.39.223-2.73.635-3.985a.75.75 0 01.722-.516l.143.001c2.996 0 5.718-1.17 7.734-3.08zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zM12 15a.75.75 0 00-.75.75v.008c0 .414.336.75.75.75h.008a.75.75 0 00.75-.75v-.008a.75.75 0 00-.75-.75H12z" clip-rule="evenodd"/>
</svg>
Outdated
</span>
</span>
<!-- danger Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-danger bg-surface text-xs font-medium text-danger dark:border-danger dark:bg-surface-dark dark:text-danger">
<span class="flex items-center gap-1 bg-danger/10 px-2 py-1 dark:bg-danger/10">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-4">
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" clip-rule="evenodd"/>
</svg>
Disconnected
</span>
</span>

View File

@@ -0,0 +1,64 @@
<!-- default Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-outline bg-surface text-xs font-medium text-on-surface dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
<span class="flex items-center gap-1 bg-surface-alt/10 px-2 py-1 dark:bg-surface-dark-alt/10">
<span class="size-1.5 rounded-full bg-on-surface dark:bg-on-surface-dark"></span>
Bagde
</span>
</span>
<!-- inverse Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-outline-dark bg-surface text-xs font-medium text-on-surface dark:border-outline dark:bg-surface-dark dark:text-on-surface-dark">
<span class="flex items-center gap-1 bg-surface-dark-alt/10 px-2 py-1 dark:bg-surface-alt/10">
<span class="size-1.5 rounded-full bg-on-surface dark:bg-on-surface-dark"></span>
Bagde
</span>
</span>
<!-- primary Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-primary bg-surface text-xs font-medium text-primary dark:border-primary-dark dark:bg-surface-dark dark:text-primary-dark">
<span class="flex items-center gap-1 bg-primary/10 px-2 py-1 dark:bg-primary-dark/10">
<span class="size-1.5 rounded-full bg-primary dark:bg-primary-dark"></span>
Bagde
</span>
</span>
<!-- secondary Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-secondary bg-surface text-xs font-medium text-secondary dark:border-secondary-dark dark:bg-surface-dark dark:text-secondary-dark">
<span class="flex items-center gap-1 bg-secondary/10 px-2 py-1 dark:bg-secondary-dark/10">
<span class="size-1.5 rounded-full bg-secondary dark:bg-secondary-dark"></span>
Bagde
</span>
</span>
<!-- info Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-info bg-surface text-xs font-medium text-info dark:border-info dark:bg-surface-dark dark:text-info">
<span class="flex items-center gap-1 bg-info/10 px-2 py-1 dark:bg-info/10">
<span class="size-1.5 rounded-full bg-info dark:bg-info"></span>
Bagde
</span>
</span>
<!-- success Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-success bg-surface text-xs font-medium text-success dark:border-success dark:bg-surface-dark dark:text-success">
<span class="flex items-center gap-1 bg-success/10 px-2 py-1 dark:bg-success/10">
<span class="size-1.5 rounded-full bg-success dark:bg-success"></span>
Bagde
</span>
</span>
<!-- warning Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-warning bg-surface text-xs font-medium text-warning dark:border-warning dark:bg-surface-dark dark:text-warning">
<span class="flex items-center gap-1 bg-warning/10 px-2 py-1 dark:bg-warning/10">
<span class="size-1.5 rounded-full bg-warning dark:bg-warning"></span>
Bagde
</span>
</span>
<!-- danger Badge -->
<span class="w-fit inline-flex overflow-hidden rounded-radius border border-danger bg-surface text-xs font-medium text-danger dark:border-danger dark:bg-surface-dark dark:text-danger">
<span class="flex items-center gap-1 bg-danger/10 px-2 py-1 dark:bg-danger/10">
<span class="size-1.5 rounded-full bg-danger dark:bg-danger"></span>
Bagde
</span>
</span>

View File

@@ -0,0 +1,24 @@
<!-- default Badge -->
<span class="rounded-radius w-fit border border-outline bg-surface-alt px-2 py-1 text-xs font-medium text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark">Bagde</span>
<!-- inverse Badge -->
<span class="rounded-radius w-fit border border-outline-dark bg-surface-dark-alt px-2 py-1 text-xs font-medium text-on-surface-dark dark:border-outline dark:bg-surface-alt dark:text-on-surface">Bagde</span>
<!-- primary Badge -->
<span class="rounded-radius w-fit border border-primary bg-primary px-2 py-1 text-xs font-medium text-on-primary dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary">Bagde</span>
<!-- secondary Badge -->
<span class="rounded-radius w-fit border border-secondary bg-secondary px-2 py-1 text-xs font-medium text-on-secondary dark:border-secondary-dark dark:bg-secondary-dark dark:text-on-secondary-dark">Bagde</span>
<!-- info Badge -->
<span class="rounded-radius w-fit border border-info bg-info px-2 py-1 text-xs font-medium text-on-info dark:border-info dark:bg-info dark:text-on-info">Bagde</span>
<!-- success Badge -->
<span class="rounded-radius w-fit border border-success bg-success px-2 py-1 text-xs font-medium text-on-success dark:border-success dark:bg-success dark:text-on-success">Bagde</span>
<!-- warning Badge -->
<span class="rounded-radius w-fit border border-warning bg-warning px-2 py-1 text-xs font-medium text-on-warning dark:border-warning dark:bg-warning dark:text-on-warning">Bagde</span>
<!-- danger Badge -->
<span class="rounded-radius w-fit border border-danger bg-danger px-2 py-1 text-xs font-medium text-on-danger dark:border-danger dark:bg-danger dark:text-on-danger">Bagde</span>

View File

@@ -0,0 +1,14 @@
<button class="relative w-fit text-on-surface dark:text-on-surface-dark" aria-label="notifications">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-8">
<path fill-rule="evenodd" d="M5.25 9a6.75 6.75 0 0113.5 0v.75c0 2.123.8 4.057 2.118 5.52a.75.75 0 01-.297 1.206c-1.544.57-3.16.99-4.831 1.243a3.75 3.75 0 11-7.48 0 24.585 24.585 0 01-4.831-1.244.75.75 0 01-.298-1.205A8.217 8.217 0 005.25 9.75V9zm4.502 8.9a2.25 2.25 0 104.496 0 25.057 25.057 0 01-4.496 0z" clip-rule="evenodd"/>
</svg>
<span class="sr-only">notifications</span>
<span class="absolute left-1/2 -top-1 rounded-full bg-danger px-1 leading-4 text-xs font-medium text-on-danger">99+</span>
</button>
<button class="relative w-fit text-on-surface dark:text-on-surface-dark" aria-label="messages">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="size-8">
<path fill-rule="evenodd" d="M4.848 2.771A49.144 49.144 0 0112 2.25c2.43 0 4.817.178 7.152.52 1.978.292 3.348 2.024 3.348 3.97v6.02c0 1.946-1.37 3.678-3.348 3.97-1.94.284-3.916.455-5.922.505a.39.39 0 00-.266.112L8.78 21.53A.75.75 0 017.5 21v-3.955a48.842 48.842 0 01-2.652-.316c-1.978-.29-3.348-2.024-3.348-3.97V6.741c0-1.946 1.37-3.68 3.348-3.97z" clip-rule="evenodd"/>
</svg>
<span class="absolute -right-0.5 top-0 size-3 rounded-full bg-danger text-xs font-medium"></span>
</button>

View File

@@ -0,0 +1,11 @@
<div class="relative flex border-outline bg-surface-alt p-4 text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark border-b">
<div class="mx-auto flex flex-wrap items-center gap-2 px-6">
<p class="sm:text-sm text-pretty text-xs">Get Fit Anywhere, Anytime 💪</p>
<button type="button" class="whitespace-nowrap bg-primary px-4 py-1 text-center text-xs font-medium tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark rounded-radius">Start free trial</button>
</div>
<button class="absolute top-1/2 -translate-y-1/2 right-4" aria-label="dismiss banner">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2.5" class="size-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>

View File

@@ -0,0 +1,22 @@
<div class="flex max-w-sm flex-col gap-4 border-outline bg-surface-alt/50 text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark rounded-radius border">
<!-- Header -->
<div class="flex items-center justify-between border-b border-outline bg-surface-alt/60 px-4 py-2 dark:border-outline-dark dark:bg-surface-dark-alt/20">
<h3 class="flex items-center gap-2 font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">
<span class="text-3xl" aria-hidden="true">🍪</span>
Cookie Time!
</h3>
</div>
<!-- Body -->
<div class="px-4 text-sm">
<p class="text-pretty">
We use cookies to make your experience sweet and crispy. For more information, please read our <a href="#" class="cursor-pointer font-medium text-primary underline-offset-2 hover:underline focus:underline focus:outline-hidden dark:text-primary-dark">Privacy Policy</a>
</p>
</div>
<!-- Footer -->
<div class="flex flex-col-reverse justify-end gap-2 border-t border-outline bg-surface-alt/60 px-4 py-2 sm:flex-row sm:items-center dark:border-outline-dark dark:bg-surface-dark/20">
<button type="button" class="cursor-pointer whitespace-nowrap p-2 text-center text-xs font-medium tracking-wide text-on-surface transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark rounded-radius">No, thank you</button>
<button type="button" class="cursor-pointer whitespace-nowrap bg-primary px-4 py-2 border border-primary text-center text-xs font-medium tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 dark:bg-primary-dark dark:text-on-primary-dark dark:border-primary-dark dark:focus-visible:outline-primary-dark rounded-radius">Sounds Good!</button>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<div class="fixed inset-x-0 top-0 z-10 flex border-outline bg-surface-alt p-4 text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark border-b">
<p class="px-6 text-xs sm:text-sm text-pretty mx-auto">Limited Time Offer! Explore exclusive <a href="#" class="font-medium text-primary underline-offset-2 hover:underline focus:underline focus:outline-hidden dark:text-primary-dark">deals & savings</a> </p>
<button class="absolute top-1/2 -translate-y-1/2 right-4" aria-label="dismiss banner">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2.5" class="size-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>

View File

@@ -0,0 +1,8 @@
<div class="relative flex border-outline bg-surface-alt p-4 text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark border-b">
<p class="px-6 text-xs sm:text-sm text-pretty mx-auto">Limited Time Offer! Explore exclusive <a href="#" class="font-medium text-primary underline-offset-2 hover:underline focus:underline focus:outline-hidden dark:text-primary-dark">deals & savings</a> </p>
<button class="absolute top-1/2 -translate-y-1/2 right-4" aria-label="dismiss banner">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2.5" class="size-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>

View File

@@ -0,0 +1,17 @@
<nav class="text-sm font-medium text-on-surface dark:text-on-surface-dark" aria-label="breadcrumb">
<ol class="flex flex-wrap items-center gap-1">
<li class="flex items-center gap-1">
<a href="#" class="hover:text-on-surface-strong dark:hover:text-on-surface-dark-strong">Home</a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true" stroke-width="2" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</li>
<li class="flex items-center gap-1">
<a href="#" class="hover:text-on-surface-strong dark:hover:text-on-surface-dark-strong">Components</a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true" stroke-width="2" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</li>
<li class="flex items-center text-on-surface-strong gap-1 font-bold dark:text-on-surface-dark-strong" aria-current="page">Breadcrumb</li>
</ol>
</nav>

View File

@@ -0,0 +1,21 @@
<nav class="text-sm font-medium text-on-surface dark:text-on-surface-dark" aria-label="breadcrumb">
<ol class="flex flex-wrap items-center gap-1">
<li class="flex items-center gap-1.5">
<a href="#" aira-label="home" class="hover:text-on-surface-strong dark:hover:text-on-surface-dark-strong">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true" class="size-4">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.293 2.293a1 1 0 0 1 1.414 0l7 7A1 1 0 0 1 17 11h-1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6H3a1 1 0 0 1-.707-1.707l7-7Z" />
</svg>
</a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true" stroke-width="2" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</li>
<li class="flex items-center gap-1">
<a href="#" class="hover:text-on-surface-strong dark:hover:text-on-surface-dark-strong">Components</a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true" stroke-width="2" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</li>
<li class="flex items-center gap-1 font-bold text-on-surface-strong dark:text-on-surface-dark-strong" aria-current="page">Breadcrumb</li>
</ol>
</nav>

View File

@@ -0,0 +1,13 @@
<nav class="text-sm font-medium text-on-surface dark:text-on-surface-dark" aria-label="breadcrumb">
<ol class="flex flex-wrap items-center gap-2">
<li class="flex items-center gap-2">
<a href="#" class="hover:text-on-surface-strong dark:hover:text-on-surface-dark-strong">Home</a>
<span aria-hidden="true">/</span>
</li>
<li class="flex items-center gap-2">
<a href="#" class="hover:text-on-surface-strong dark:hover:text-on-surface-dark-strong">Components</a>
<span aria-hidden="true">/</span>
</li>
<li class="text-on-surface-strong font-bold dark:text-on-surface-dark-strong" aria-current="page">Breadcrumb</li>
</ol>
</nav>

View File

@@ -0,0 +1,56 @@
<!-- primary Floating Button -->
<button aria-label="create something epic" type="button" class="inline-flex justify-center items-center aspect-square whitespace-nowrap rounded-full border border-primary bg-primary p-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 fill-on-primary dark:fill-on-primary-dark" fill="currentColor">
<path fill-rule="evenodd" d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" clip-rule="evenodd" />
</svg>
</button>
<!-- secondary Floating Button -->
<button aria-label="create something epic" type="button" class="inline-flex justify-center items-center aspect-square whitespace-nowrap rounded-full border border-secondary bg-secondary p-2 text-sm font-medium tracking-wide text-on-secondary transition hover:opacity-75 text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:border-secondary-dark dark:bg-secondary-dark dark:text-on-secondary-dark dark:focus-visible:outline-secondary-dark">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 fill-on-secondary dark:fill-on-secondary-dark" fill="currentColor">
<path fill-rule="evenodd" d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" clip-rule="evenodd" />
</svg>
</button>
<!-- alternate Floating Button -->
<button aria-label="create something epic" type="button" class="inline-flex justify-center items-center aspect-square whitespace-nowrap rounded-full border border-surface-alt bg-surface-alt p-2 text-sm font-medium tracking-wide text-on-surface-strong transition hover:opacity-75 text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface-alt active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:border-surface-dark-alt dark:bg-surface-dark-alt dark:text-on-surface-dark-strong dark:focus-visible:outline-surface-dark-alt">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 fill-on-surface-strong dark:fill-on-surface-dark-strong" fill="currentColor">
<path fill-rule="evenodd" d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" clip-rule="evenodd" />
</svg>
</button>
<!-- inverse Floating Button -->
<button aria-label="create something epic" type="button" class="inline-flex justify-center items-center aspect-square whitespace-nowrap rounded-full border border-surface-dark bg-surface-dark p-2 text-sm font-medium tracking-wide text-on-surface-dark transition hover:opacity-75 text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface-dark active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:border-surface dark:bg-surface dark:text-on-surface dark:focus-visible:outline-surface">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 fill-on-surface-dark dark:fill-on-surface" fill="currentColor">
<path fill-rule="evenodd" d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" clip-rule="evenodd" />
</svg>
</button>
<!-- info Floating Button -->
<button aria-label="create something epic" type="button" class="inline-flex justify-center items-center aspect-square whitespace-nowrap rounded-full border border-info bg-info p-2 text-sm font-medium tracking-wide text-on-info transition hover:opacity-75 text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:border-info dark:bg-info dark:text-on-info dark:focus-visible:outline-info">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 fill-on-info dark:fill-on-info" fill="currentColor">
<path fill-rule="evenodd" d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" clip-rule="evenodd" />
</svg>
</button>
<!-- danger Floating Button -->
<button aria-label="create something epic" type="button" class="inline-flex justify-center items-center aspect-square whitespace-nowrap rounded-full border border-danger bg-danger p-2 text-sm font-medium tracking-wide text-on-danger transition hover:opacity-75 text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:border-danger dark:bg-danger dark:text-on-danger dark:focus-visible:outline-danger">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 fill-on-danger dark:fill-on-danger" fill="currentColor">
<path fill-rule="evenodd" d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" clip-rule="evenodd" />
</svg>
</button>
<!-- warning Floating Button -->
<button aria-label="create something epic" type="button" class="inline-flex justify-center items-center aspect-square whitespace-nowrap rounded-full border border-warning bg-warning p-2 text-sm font-medium tracking-wide text-on-warning transition hover:opacity-75 text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-warning active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:border-warning dark:bg-warning dark:text-on-warning dark:focus-visible:outline-warning">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 fill-on-warning dark:fill-on-warning" fill="currentColor">
<path fill-rule="evenodd" d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" clip-rule="evenodd" />
</svg>
</button>
<!-- success Floating Button -->
<button aria-label="create something epic" type="button" class="inline-flex justify-center items-center aspect-square whitespace-nowrap rounded-full border border-success bg-success p-2 text-sm font-medium tracking-wide text-on-success transition hover:opacity-75 text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-success active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:border-success dark:bg-success dark:text-on-success dark:focus-visible:outline-success">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 fill-on-success dark:fill-on-success" fill="currentColor">
<path fill-rule="evenodd" d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" clip-rule="evenodd" />
</svg>
</button>

Some files were not shown because too many files have changed in this diff Show More