25 Commits

Author SHA1 Message Date
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
Priec
0754e014a3 more hardcodes are done 2026-06-17 21:52:43 +02:00
Priec
1d747d9960 sidebar is no penguin ui 2026-06-17 21:24:45 +02:00
Priec
126b1eeb7e product card is now pengui ui 2026-06-17 21:16:28 +02:00
Priec
c401acb1cc toast is now from the penguin ui library and it looks so good 2026-06-17 21:02:18 +02:00
Priec
67fd364761 hardcoded toast 2026-06-17 20:12:31 +02:00
262 changed files with 13534 additions and 977 deletions

2
.gitignore vendored
View File

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

1599
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
[package]
name = "kompress_eshop"
version = "0.1.0"
edition = "2021"
edition = "2024"
publish = false
default-run = "kompress-eshop-cli"
@@ -16,14 +16,14 @@ loco-rs = { version = "0.16" }
loco-rs = { workspace = true }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" }
tokio = { version = "1.45", default-features = false, features = [
tokio = { version = "1.52", default-features = false, features = [
"rt-multi-thread",
] }
async-trait = { version = "0.1" }
axum = { version = "0.8", features = ["multipart"] }
tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
regex = { version = "1.11" }
regex = { version = "1.12" }
migration = { path = "migration" }
sea-orm = { version = "1.1", features = [
"sqlx-sqlite",
@@ -35,7 +35,7 @@ chrono = { version = "0.4" }
time = { version = "0.3" }
dotenvy = { version = "0.15" }
validator = { version = "0.20" }
uuid = { version = "1.6", features = ["v4"] }
uuid = { version = "1.23", features = ["v4"] }
include_dir = { version = "0.7" }
# outbound HTTP for carrier shipment APIs (Packeta / DPD / DHL)
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
@@ -45,6 +45,10 @@ unic-langid = { version = "0.9" }
# /view engine
axum-extra = { version = "0.10", features = ["form"] }
bytes = { version = "1" }
axum-casbin = "1.3.0"
loco-oauth2 = "0.5.0"
passwords = "3.1.16"
tower-sessions = "0.14"
[[bin]]
name = "kompress-eshop-cli"
@@ -53,6 +57,6 @@ required-features = []
[dev-dependencies]
loco-rs = { workspace = true, features = ["testing"] }
serial_test = { version = "3.1.1" }
serial_test = { version = "3.5.0" }
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. */
@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
* already sets <html data-theme="dark|light"> (see base.html), so
* 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-queue-all = queue all tracks in order
album-no-tracks = no tracks yet
login-title = Admin login
login-title = Sign in
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-auth = Authenticate
login-auth = Sign in
login-email = Email
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
admin-session = Session
readonly = readonly
@@ -202,8 +221,11 @@ parent-category = Parent category
no-parent = — None (top level) —
quantity = Quantity
add-to-cart = Add to cart
cart-added = Added to cart
in-stock = In stock
out-of-stock = Out of stock
gallery-prev = Previous image
gallery-next = Next image
confirm-delete = Delete this for good?
shop-title = Shop
shop-subtitle = browse our products.
@@ -235,8 +257,35 @@ country-de = Germany
country-pl = Poland
country-hu = Hungary
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-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Č.
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.
order-confirmed-title = Thank you for your order!
order-confirmed-sub = We have received your order.
order-number = Order number

View File

@@ -57,12 +57,31 @@ album-by = od
album-play-full = Prehrať celý album
album-queue-all = zoradiť všetky skladby v poradí
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-unverified = Účet ešte nie je overený. Skontrolujte si e-mail a kliknite na overovací odkaz.
login-root = root
login-auth = Prihlásiť sa
login-email = E-mail
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
admin-session = Relácia
readonly = iba na čítanie
@@ -202,8 +221,11 @@ parent-category = Nadradená kategória
no-parent = — Žiadna (najvyššia úroveň) —
quantity = Množstvo
add-to-cart = Pridať do košíka
cart-added = Pridané do košíka
in-stock = Na sklade
out-of-stock = Vypredané
gallery-prev = Predchádzajúci obrázok
gallery-next = Ďalší obrázok
confirm-delete = Naozaj zmazať?
shop-title = Obchod
shop-subtitle = prezrite si našu ponuku produktov.
@@ -235,8 +257,35 @@ country-de = Nemecko
country-pl = Poľsko
country-hu = Maďarsko
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-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Č.
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ú.
order-confirmed-title = Ďakujeme za objednávku!
order-confirmed-sub = Vašu objednávku sme prijali.
order-number = Číslo objednávky

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,155 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="profile-title", lang=lang | default(value='sk')) }}{% endblock title %}
{% block content %}
<div class="mx-auto max-w-2xl">
<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 %}
<form 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="space-y-1.5">
<label 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>
<p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">{{ name }}</p>
</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>
{{ ui::button(label=t(key="profile-save", lang=lang | default(value='sk')), type="submit", size="px-6 py-2.5 text-sm") }}
</form>
</div>
{% endblock content %}

View File

@@ -1,3 +1,4 @@
{% import "macros/ui.html" as ui %}
<!doctype html>
<html lang="{{ lang | default(value='sk') }}" data-theme="dark">
<head>
@@ -57,6 +58,11 @@
x-bind:class="showSidebar ? 'translate-x-0' : '-translate-x-60'"
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
penguinui-components/sidebar/simple-sidebar.html: Penguin's link
treatment (hover:bg-primary/5, focus-visible:underline) with the active
state (bg-primary/10 + text-on-surface-strong) mapped onto our
data-nav / aria-current so markActiveNav() keeps driving it. #}
<a href="/admin/dashboard"
class="flex h-16 items-center gap-2 border-b border-outline px-6 text-lg font-bold tracking-tight text-on-surface-strong dark:border-outline-dark dark:text-on-surface-dark-strong">
{{ t(key="admin-title", lang=lang | default(value='sk')) }}
@@ -64,33 +70,33 @@
<div class="flex flex-1 flex-col gap-1 overflow-y-auto p-4">
<a href="/admin/dashboard" data-nav="/admin/dashboard"
class="flex items-center gap-3 rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ t(key="admin-dashboard", lang=lang | default(value='sk')) }}
</a>
<a href="/admin/catalog/products" data-nav="/admin/catalog/products"
class="flex items-center gap-3 rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ t(key="admin-products", lang=lang | default(value='sk')) }}
</a>
<a href="/admin/catalog/categories" data-nav="/admin/catalog/categories"
class="flex items-center gap-3 rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ t(key="admin-categories", lang=lang | default(value='sk')) }}
</a>
<a href="/admin/orders" data-nav="/admin/orders"
class="flex items-center gap-3 rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ t(key="admin-orders", lang=lang | default(value='sk')) }}
</a>
<a href="/admin/shipping" data-nav="/admin/shipping"
class="flex items-center gap-3 rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ t(key="admin-shipping", lang=lang | default(value='sk')) }}
</a>
</div>
<div class="border-t border-outline p-4 dark:border-outline-dark">
<a href="/" class="flex items-center gap-3 rounded-radius px-3 py-2 text-sm font-medium text-info transition hover:bg-surface dark:hover:bg-surface-dark">
<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')) }}
</a>
<form method="post" action="/admin/logout">
<button type="submit" class="flex w-full items-center gap-3 rounded-radius px-3 py-2 text-left text-sm font-medium text-danger transition hover:bg-surface dark:hover:bg-surface-dark">
<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">
{{ t(key="logout", lang=lang | default(value='sk')) }}
</button>
</form>
@@ -100,13 +106,11 @@
<!-- content column -->
<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">
<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">
<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>
<!-- Penguin animated hamburger (bars ↔ X) in our ghost-square shell -->
<button type="button" @click="showSidebar = !showSidebar" :aria-expanded="showSidebar" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
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">
{{ ui::icon(name="hamburger", size="size-6", attrs='x-show="!showSidebar"') }}
{{ ui::icon(name="close", size="size-6", attrs='x-cloak x-show="showSidebar"') }}
</button>
<span class="text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">
@@ -115,54 +119,7 @@
<!-- settings (language + theme) dropdown -->
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative ml-auto">
<button type="button" @click="open = !open" :aria-expanded="open"
aria-label="{{ 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 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-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
class="absolute right-0 mt-2 w-56 rounded-radius border border-outline bg-surface p-2 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt">
<form method="post" action="/lang" hx-boost="false">
<p class="px-2 py-1 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
{{ t(key="settings-language", lang=lang | default(value='sk')) }}
</p>
<button type="submit" name="lang" value="en"
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
<span>English</span>
{% if lang | default(value='sk') == "en" %}<span class="text-primary dark:text-primary-dark"></span>{% endif %}
</button>
<button type="submit" name="lang" value="sk"
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
<span>Slovenčina</span>
{% if lang | default(value='sk') == "sk" %}<span class="text-primary dark:text-primary-dark"></span>{% endif %}
</button>
</form>
<p class="mt-1 px-2 py-1 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')) }}
</p>
<div x-data="{ theme: currentTheme() }" @theme:changed.document="theme = $event.detail">
<button type="button" @click="setTheme('system')"
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
<span>{{ t(key="theme-system", lang=lang | default(value='sk')) }}</span>
<span x-show="theme === 'system'" class="text-primary dark:text-primary-dark"></span>
</button>
<button type="button" @click="setTheme('light')"
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
<span>{{ t(key="theme-light", lang=lang | default(value='sk')) }}</span>
<span x-show="theme === 'light'" class="text-primary dark:text-primary-dark"></span>
</button>
<button type="button" @click="setTheme('dark')"
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
<span>{{ t(key="theme-dark", lang=lang | default(value='sk')) }}</span>
<span x-show="theme === 'dark'" class="text-primary dark:text-primary-dark"></span>
</button>
</div>
</div>
{% include "partials/settings_dropdown.html" %}
</div>
</header>

View File

@@ -1,4 +1,5 @@
{% extends "admin/base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="admin-categories", lang=lang | default(value='sk')) }}{% endblock title %}
{% block crumb %}{{ t(key="admin-categories", lang=lang | default(value='sk')) }}{% endblock crumb %}
@@ -9,26 +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>
<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>
<a href="/admin/catalog/categories/new"
class="inline-flex items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="new-category", lang=lang | default(value='sk')) }}
</a>
{{ ui::button(label=t(key="new-category", lang=lang | default(value='sk')), href="/admin/catalog/categories/new") }}
</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 %}
<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 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
<table class="{{ ui::table_cls() }}">
<thead class="{{ ui::thead_cls() }}">
<tr>
<th class="px-4 py-3 font-semibold">{{ t(key="name", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 font-semibold">{{ t(key="admin-products", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 font-semibold">{{ t(key="status", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 text-right font-semibold">{{ t(key="actions", lang=lang | default(value='sk')) }}</th>
{{ ui::th(label=t(key="name", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="admin-products", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="status", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="actions", lang=lang | default(value='sk')), align="text-right") }}
</tr>
</thead>
<tbody class="divide-y divide-outline dark:divide-outline-dark">
<tbody class="{{ ui::tbody_cls() }}">
{% 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">
<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 %}
@@ -38,18 +36,17 @@
<td class="px-4 py-3 tabular-nums">{{ row.product_count }}</td>
<td class="px-4 py-3">
{% if row.category.published %}
<span class="inline-flex rounded-full bg-success/15 px-2 py-0.5 text-xs font-medium text-success">{{ t(key="published", lang=lang | default(value='sk')) }}</span>
{{ ui::badge(label=t(key="published", lang=lang | default(value='sk')), variant="success") }}
{% else %}
<span class="inline-flex rounded-full bg-surface-alt px-2 py-0.5 text-xs font-medium text-on-surface/70 dark:bg-surface-dark-alt dark:text-on-surface-dark/70">{{ t(key="draft", lang=lang | default(value='sk')) }}</span>
{{ ui::badge(label=t(key="draft", lang=lang | default(value='sk')), variant="neutral") }}
{% endif %}
</td>
<td class="px-4 py-3">
<div class="flex flex-wrap justify-end gap-2">
<a href="/admin/catalog/categories/{{ row.category.id }}/edit"
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="edit", lang=lang | default(value='sk')), href="/admin/catalog/categories/" ~ row.category.id ~ "/edit", size="px-3 py-1.5 text-xs") }}
<form method="post" action="/admin/catalog/categories/{{ row.category.id }}/delete"
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>
</div>
</td>
@@ -60,10 +57,7 @@
{% else %}
<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>
<a href="/admin/catalog/categories/new"
class="inline-flex items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="new-category", lang=lang | default(value='sk')) }}
</a>
{{ ui::button(label=t(key="new-category", lang=lang | default(value='sk')), href="/admin/catalog/categories/new") }}
</div>
{% endif %}
</div>

View File

@@ -1,4 +1,5 @@
{% extends "admin/base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{% if category %}{{ t(key="edit-category", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-category", lang=lang | default(value='sk')) }}{% endif %}{% endblock title %}
{% block crumb %}{{ t(key="admin-categories", lang=lang | default(value='sk')) }}{% endblock crumb %}
@@ -8,51 +9,54 @@
<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 %}
</h1>
<a href="/admin/catalog/categories"
class="inline-flex items-center rounded-radius border border-outline px-3 py-2 text-sm 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="cancel", lang=lang | default(value='sk')) }}</a>
{{ 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>
<form method="post" enctype="multipart/form-data"
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">
{% 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">
<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 %}"
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">
{{ ui::input(name="name", id="name", required=true, value=v_name) }}
</div>
<div class="grid gap-5 sm:grid-cols-2">
<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>
<input id="slug" name="slug" type="text" value="{% if category %}{{ category.slug }}{% endif %}"
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">
{{ ui::input(name="slug", id="slug", value=v_slug, placeholder=t(key='slug-auto', lang=lang | default(value='sk'))) }}
</div>
<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>
<input id="position" name="position" type="number" value="{% if category %}{{ category.position }}{% else %}0{% endif %}"
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">
{{ ui::input(name="position", id="position", type="number", value=v_pos) }}
</div>
</div>
<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>
<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">
<option value="">{{ t(key="no-parent", lang=lang | default(value='sk')) }}</option>
{% for parent in parents %}
<option value="{{ parent.id }}" {% if category and category.parent_id == parent.id %}selected{% endif %}>
{% if parent.depth > 0 %}{% for _ in range(end=parent.depth) %}—&nbsp;{% endfor %}{% endif %}{{ parent.name }}
</option>
{% endfor %}
</select>
<div class="relative">
<select id="parent_id" name="parent_id"
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>
{% for parent in parents %}
<option value="{{ parent.id }}" {% if category and category.parent_id == parent.id %}selected{% endif %}>
{% if parent.depth > 0 %}{% for _ in range(end=parent.depth) %}—&nbsp;{% endfor %}{% endif %}{{ parent.name }}
</option>
{% endfor %}
</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 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>
<textarea id="description" name="description" rows="4"
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>
{{ ui::textarea(name="description", id="description", rows="4", value=v_desc) }}
</div>
<div class="space-y-1.5">
@@ -60,23 +64,14 @@
{% if category and category.image_id %}
<img src="/images/{{ category.image_id }}" alt="" class="size-24 rounded-radius object-cover">
{% endif %}
<input id="image" name="image" type="file" 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">
{{ ui::file_input(name="image", id="image", accept="image/*") }}
</div>
<label class="flex items-center gap-2">
<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>
{{ ui::checkbox(name="published", id="published", label=t(key="published", lang=lang | default(value='sk')), checked=v_pub) }}
<div class="flex gap-3 pt-2">
<button type="submit"
class="inline-flex items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="save", lang=lang | default(value='sk')) }}
</button>
<a href="/admin/catalog/categories"
class="inline-flex items-center rounded-radius border border-outline px-4 py-2 text-sm 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="cancel", lang=lang | default(value='sk')) }}</a>
{{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit") }}
{{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/categories") }}
</div>
</form>
{% endblock content %}

View File

@@ -1,4 +1,5 @@
{% extends "admin/base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{% if product %}{{ t(key="edit-product", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-product", lang=lang | default(value='sk')) }}{% endif %}{% endblock title %}
{% block crumb %}{{ t(key="admin-products", lang=lang | default(value='sk')) }}{% endblock crumb %}
@@ -8,69 +9,68 @@
<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 %}
</h1>
<a href="/admin/catalog/products"
class="inline-flex items-center rounded-radius border border-outline px-3 py-2 text-sm 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="cancel", lang=lang | default(value='sk')) }}</a>
{{ 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>
<form method="post" enctype="multipart/form-data"
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">
{% 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">
<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 %}"
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">
{{ ui::input(name="name", id="name", required=true, value=v_name) }}
</div>
<div class="grid gap-5 sm:grid-cols-2">
<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>
<input id="price" name="price" type="text" inputmode="decimal" required value="{% if product %}{{ product.price }}{% endif %}"
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">
{{ ui::input(name="price", id="price", required=true, value=v_price, placeholder="0.00", attrs='inputmode="decimal"') }}
</div>
<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>
<input id="currency" name="currency" type="text" maxlength="3" value="{% if product %}{{ product.currency }}{% else %}EUR{% endif %}"
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">
{{ ui::input(name="currency", id="currency", value=v_currency, attrs='maxlength="3"', extra="uppercase") }}
</div>
</div>
<div class="grid gap-5 sm:grid-cols-2">
<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>
<input id="stock" name="stock" type="number" min="0" value="{% if product %}{{ product.stock }}{% else %}0{% endif %}"
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">
{{ ui::input(name="stock", id="stock", type="number", value=v_stock, attrs='min="0"') }}
</div>
<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>
<input id="sku" name="sku" type="text" value="{% if product and product.sku %}{{ product.sku }}{% endif %}"
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">
{{ ui::input(name="sku", id="sku", value=v_sku) }}
</div>
</div>
<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>
<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">
<option value="">{{ t(key="no-category", lang=lang | default(value='sk')) }}</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if product and product.category_id == category.id %}selected{% endif %}>{{ category.name }}</option>
{% endfor %}
</select>
<div class="relative">
<select id="category_id" name="category_id"
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>
{% for category in categories %}
<option value="{{ category.id }}" {% if product and product.category_id == category.id %}selected{% endif %}>{{ category.name }}</option>
{% endfor %}
</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 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>
<input id="slug" name="slug" type="text" value="{% if product %}{{ product.slug }}{% endif %}"
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">
{{ ui::input(name="slug", id="slug", value=v_slug, placeholder=t(key='slug-auto', lang=lang | default(value='sk'))) }}
</div>
<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>
<textarea id="description" name="description" rows="5"
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>
{{ ui::textarea(name="description", id="description", rows="5", value=v_desc) }}
</div>
<div class="space-y-1.5">
@@ -78,23 +78,14 @@
{% if product and product.image %}
<img src="/images/{{ product.image }}" alt="" class="size-24 rounded-radius object-cover">
{% endif %}
<input id="image" name="image" type="file" 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">
{{ ui::file_input(name="image", id="image", accept="image/*") }}
</div>
<label class="flex items-center gap-2">
<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>
{{ ui::checkbox(name="published", id="published", label=t(key="published", lang=lang | default(value='sk')), checked=v_pub) }}
<div class="flex gap-3 pt-2">
<button type="submit"
class="inline-flex items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="save", lang=lang | default(value='sk')) }}
</button>
<a href="/admin/catalog/products"
class="inline-flex items-center rounded-radius border border-outline px-4 py-2 text-sm 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="cancel", lang=lang | default(value='sk')) }}</a>
{{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit") }}
{{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/products") }}
</div>
</form>
{% endblock content %}

View File

@@ -1,4 +1,5 @@
{% extends "admin/base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="admin-products", lang=lang | default(value='sk')) }}{% endblock title %}
{% block crumb %}{{ t(key="admin-products", lang=lang | default(value='sk')) }}{% endblock crumb %}
@@ -9,27 +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>
<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>
<a href="/admin/catalog/products/new"
class="inline-flex items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="new-product", lang=lang | default(value='sk')) }}
</a>
{{ ui::button(label=t(key="new-product", lang=lang | default(value='sk')), href="/admin/catalog/products/new") }}
</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 %}
<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 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
<table class="{{ ui::table_cls() }}">
<thead class="{{ ui::thead_cls() }}">
<tr>
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 font-semibold">{{ t(key="price", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 font-semibold">{{ t(key="stock", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 font-semibold">{{ t(key="status", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 text-right font-semibold">{{ t(key="actions", lang=lang | default(value='sk')) }}</th>
{{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="price", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="stock", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="status", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="actions", lang=lang | default(value='sk')), align="text-right") }}
</tr>
</thead>
<tbody class="divide-y divide-outline dark:divide-outline-dark">
<tbody class="{{ ui::tbody_cls() }}">
{% 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">
<div class="flex items-center gap-3">
{% if product.image %}
@@ -47,20 +45,18 @@
<td class="px-4 py-3 tabular-nums">{{ product.stock }}</td>
<td class="px-4 py-3">
{% if product.published %}
<span class="inline-flex rounded-full bg-success/15 px-2 py-0.5 text-xs font-medium text-success">{{ t(key="published", lang=lang | default(value='sk')) }}</span>
{{ ui::badge(label=t(key="published", lang=lang | default(value='sk')), variant="success") }}
{% else %}
<span class="inline-flex rounded-full bg-surface-alt px-2 py-0.5 text-xs font-medium text-on-surface/70 dark:bg-surface-dark-alt dark:text-on-surface-dark/70">{{ t(key="draft", lang=lang | default(value='sk')) }}</span>
{{ ui::badge(label=t(key="draft", lang=lang | default(value='sk')), variant="neutral") }}
{% endif %}
</td>
<td class="px-4 py-3">
<div class="flex flex-wrap justify-end gap-2">
<a href="/admin/catalog/products/{{ product.id }}/edit"
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>
<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>
{{ 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") }}
{{ 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") }}
<form method="post" action="/admin/catalog/products/{{ product.id }}/delete"
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>
</div>
</td>
@@ -71,10 +67,7 @@
{% else %}
<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>
<a href="/admin/catalog/products/new"
class="inline-flex items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="new-product", lang=lang | default(value='sk')) }}
</a>
{{ ui::button(label=t(key="new-product", lang=lang | default(value='sk')), href="/admin/catalog/products/new") }}
</div>
{% endif %}
</div>

View File

@@ -1,62 +0,0 @@
{% extends "base.html" %}
{% 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>
<span
class="rounded-radius border border-danger/40 px-2 py-0.5 text-xs font-medium text-danger">
{{ t(key="auth", lang=lang | default(value='sk')) }}
</span>
</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 %}
<div
class="mt-3 rounded-radius border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger"
role="alert">
✗ {{ t(key="login-error", lang=lang | default(value='sk')) }}
</div>
{% 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>
<button type="submit"
class="mt-1 w-full rounded-radius bg-primary px-4 py-2 text-sm font-semibold text-on-primary transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-surface-alt dark:bg-primary-dark dark:text-on-primary-dark dark:focus:ring-primary-dark dark:focus:ring-offset-surface-dark-alt">
{{ t(key="login-auth", lang=lang | default(value='sk')) }}
</button>
</form>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -1,4 +1,5 @@
{% extends "admin/base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="admin-orders", lang=lang | default(value='sk')) }}{% endblock title %}
{% block crumb %}{{ t(key="admin-orders", lang=lang | default(value='sk')) }}{% endblock crumb %}
@@ -6,29 +7,29 @@
{% 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>
<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 %}
<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 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
<table class="{{ ui::table_cls() }}">
<thead class="{{ ui::thead_cls() }}">
<tr>
<th class="px-4 py-3 font-semibold">{{ t(key="order-number", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 font-semibold">{{ t(key="order-customer", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 font-semibold">{{ t(key="order-status", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 text-right font-semibold">{{ t(key="order-total", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3"></th>
{{ ui::th(label=t(key="order-number", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="order-customer", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="order-status", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="order-total", lang=lang | default(value='sk')), align="text-right") }}
{{ ui::th(label="") }}
</tr>
</thead>
<tbody class="divide-y divide-outline dark:divide-outline-dark">
<tbody class="{{ ui::tbody_cls() }}">
{% 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">{{ order.email }}</td>
<td class="px-4 py-3">
<span class="inline-flex rounded-full bg-surface-alt px-2 py-0.5 text-xs font-medium text-on-surface/80 dark:bg-surface-dark-alt dark:text-on-surface-dark/80">{{ t(key="order-status-" ~ order.status, lang=lang | default(value='sk')) }}</span>
{{ ui::badge(label=t(key="order-status-" ~ order.status, lang=lang | default(value='sk')), variant="neutral") }}
</td>
<td class="px-4 py-3 text-right tabular-nums">{{ order.total }} {{ order.currency }}</td>
<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>
</tr>
{% endfor %}

View File

@@ -1,4 +1,5 @@
{% extends "admin/base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ order.order_number }}{% endblock title %}
{% block crumb %}{{ t(key="admin-orders", lang=lang | default(value='sk')) }}{% endblock crumb %}
@@ -6,27 +7,25 @@
{% block content %}
<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>
<a href="/admin/orders" class="inline-flex items-center rounded-radius border border-outline px-3 py-2 text-sm 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="admin-orders", lang=lang | default(value='sk')) }}</a>
{{ 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>
{% if ship_error %}
<div class="mt-4 rounded-radius border border-danger/40 bg-danger/10 px-4 py-3 text-sm font-medium text-danger">
{{ ship_error }}
</div>
{{ ui::alert_danger(message=ship_error, extra="mt-4") }}
{% endif %}
<div class="mt-6 grid gap-6 lg:grid-cols-3">
<div class="space-y-6 lg:col-span-2">
<div class="overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
<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 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
<div class="{{ ui::table_wrap_cls() }}">
<table class="{{ ui::table_cls() }}">
<thead class="{{ ui::thead_cls() }}">
<tr>
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 font-semibold">{{ t(key="quantity", lang=lang | default(value='sk')) }}</th>
<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="product", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="quantity", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="order-total", lang=lang | default(value='sk')), align="text-right") }}
</tr>
</thead>
<tbody class="divide-y divide-outline dark:divide-outline-dark">
<tbody class="{{ ui::tbody_cls() }}">
{% for item in items %}
<tr>
<td class="px-4 py-3">{{ item.product_name }}</td>
@@ -35,7 +34,7 @@
</tr>
{% endfor %}
</tbody>
<tfoot class="border-t border-outline dark:border-outline-dark">
<tfoot class="{{ ui::tfoot_cls() }}">
<tr>
<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>
@@ -53,6 +52,15 @@
<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 %}
</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>
<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>
@@ -86,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>
</p>
{% if order.label_url %}
<a href="{{ order.label_url }}" 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>
{{ 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"') }}
{% endif %}
{% 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>
@@ -95,23 +102,25 @@
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-send-hint", lang=lang | default(value='sk')) }}</p>
<form method="post" action="/admin/orders/{{ order.id }}/ship"
onsubmit="return confirm('{{ t(key="order-send-confirm", lang=lang | default(value='sk')) }}')">
<button type="submit"
class="inline-flex w-full items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="order-send-to-carrier", lang=lang | default(value='sk')) }} {{ carrier | upper }}
</button>
{% set carrier_up = carrier | upper %}
{% set ship_label = t(key="order-send-to-carrier", lang=lang | default(value='sk')) ~ " " ~ carrier_up %}
{{ ui::button(label=ship_label, type="submit", extra="w-full") }}
</form>
{% endif %}
</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">
<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>
<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">
{% for status in statuses %}
<option value="{{ status }}" {% if order.status == status %}selected{% endif %}>{{ t(key="order-status-" ~ status, lang=lang | default(value='sk')) }}</option>
{% endfor %}
</select>
<button type="submit" class="inline-flex w-full items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="order-update-status", lang=lang | default(value='sk')) }}</button>
<div class="relative">
<select id="status" name="status"
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 %}
<option value="{{ status }}" {% if order.status == status %}selected{% endif %}>{{ t(key="order-status-" ~ status, lang=lang | default(value='sk')) }}</option>
{% endfor %}
</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>
{{ ui::button(label=t(key="order-update-status", lang=lang | default(value='sk')), type="submit", extra="w-full") }}
</form>
</aside>
</div>

View File

@@ -1,4 +1,5 @@
{% extends "admin/base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="admin-shipping", lang=lang | default(value='sk')) }}{% endblock title %}
{% block crumb %}{{ t(key="admin-shipping", lang=lang | default(value='sk')) }}{% endblock crumb %}
@@ -19,18 +20,10 @@
</div>
<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>
<input id="price-{{ method.id }}" name="price" type="text" inputmode="decimal" value="{{ method.price }}"
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">
{{ ui::input(name="price", id="price-" ~ method.id, value=method.price, width="w-28", attrs='inputmode="decimal"') }}
</div>
<label class="flex items-center gap-2 pb-2">
<input type="checkbox" name="enabled" value="on" {% if method.enabled %}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="shipping-enabled", lang=lang | default(value='sk')) }}</span>
</label>
<button type="submit"
class="ml-auto inline-flex items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="save", lang=lang | default(value='sk')) }}
</button>
<div class="pb-2">{{ ui::checkbox(name="enabled", label=t(key="shipping-enabled", lang=lang | default(value='sk')), checked=method.enabled) }}</div>
{{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit", extra="ml-auto") }}
</form>
{% endfor %}
</div>

View File

@@ -0,0 +1,64 @@
{% 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") }}
{% 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,87 @@
{% 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 %}
{{ 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">
<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="name"
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
{{ t(key="register-name", lang=lang | default(value='sk')) }}
</label>
{{ ui::input(name="name", id="name", required=true, autocomplete="name", attrs="autofocus") }}
</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") }}
</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") }}
</div>
{{ ui::button(label=t(key="register-submit", 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-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,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>
<html lang="{{ lang | default(value='sk') }}" data-theme="dark">
<head>
@@ -47,6 +48,12 @@
if (!v) return 0;
return v.split(',').reduce(function (s, e) { return s + (parseInt(e.split(':')[1]) || 0) }, 0);
}
// Show a floating toast notification. Usage: toast('Saved').
// Bridges to the vendored Penguin UI toast component, which listens for a
// `notify` event with { variant, title, message }.
function toast(message) {
window.dispatchEvent(new CustomEvent('notify', { detail: { variant: 'success', message: message } }));
}
</script>
<link href="/static/css/app.css?v=2026-06-16" rel="stylesheet" type="text/css">
<script src="/static/vendor/htmx/htmx-1.9.12.min.js"></script>
@@ -60,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">
<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) -->
<button type="button" @click="cats = !cats" :aria-expanded="cats"
aria-label="{{ t(key='categories', lang=lang | default(value='sk')) }}"
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>
{% set hamburger_icon = ui::icon(name="hamburger", size="size-6") %}
{{ 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) }}
<a href="/"
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')) }}
</a>
<!-- desktop links -->
<ul class="ml-2 hidden items-center gap-1 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><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>
<!-- desktop links — Penguin navbar link treatment via ui::nav_link -->
<ul class="ml-2 hidden items-center gap-6 md:flex">
<li>{{ ui::nav_link(label=t(key="nav-home", lang=lang | default(value='sk')), href="/", data_nav="/") }}</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 %}
<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>
<form method="post" action="/admin/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>
<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>
</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>
</li>
{% 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 %}
</ul>
@@ -93,95 +103,51 @@
<!-- cart with live item-count badge read from the `cart` cookie -->
<a href="/cart" data-nav="/cart"
x-data="{ count: 0 }"
x-init="count = cartCount(); window.addEventListener('htmx:afterSwap', 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')) }}"
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">
<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="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>
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">
{{ ui::icon(name="cart") }}
<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>
</a>
<!-- settings (language + theme) dropdown -->
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative">
<button type="button" @click="open = !open" :aria-expanded="open"
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 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-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
class="absolute right-0 mt-2 w-56 rounded-radius border border-outline bg-surface p-2 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt">
<form method="post" action="/lang" hx-boost="false">
<p class="px-2 py-1 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
{{ t(key="settings-language", lang=lang | default(value='sk')) }}
</p>
<button type="submit" name="lang" value="en"
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
<span>English</span>
{% if lang | default(value='sk') == "en" %}<span class="text-primary dark:text-primary-dark"></span>{% endif %}
</button>
<button type="submit" name="lang" value="sk"
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
<span>Slovenčina</span>
{% if lang | default(value='sk') == "sk" %}<span class="text-primary dark:text-primary-dark"></span>{% endif %}
</button>
</form>
<p class="mt-1 px-2 py-1 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')) }}
</p>
<div x-data="{ theme: currentTheme() }" @theme:changed.document="theme = $event.detail">
<button type="button" @click="setTheme('system')"
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
<span>{{ t(key="theme-system", lang=lang | default(value='sk')) }}</span>
<span x-show="theme === 'system'" class="text-primary dark:text-primary-dark"></span>
</button>
<button type="button" @click="setTheme('light')"
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
<span>{{ t(key="theme-light", lang=lang | default(value='sk')) }}</span>
<span x-show="theme === 'light'" class="text-primary dark:text-primary-dark"></span>
</button>
<button type="button" @click="setTheme('dark')"
class="flex w-full items-center justify-between rounded-radius px-2 py-1.5 text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark">
<span>{{ t(key="theme-dark", lang=lang | default(value='sk')) }}</span>
<span x-show="theme === 'dark'" class="text-primary dark:text-primary-dark"></span>
</button>
</div>
</div>
{% include "partials/settings_dropdown.html" %}
</div>
<!-- mobile hamburger -->
<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">
<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>
<!-- mobile hamburger — Penguin animated icon swap (bars ↔ X), kept in
our ghost-square icon-button shell for consistency with cart/gear -->
<button type="button" @click="mobile = !mobile" :aria-expanded="mobile" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
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">
{{ ui::icon(name="hamburger", size="size-6", attrs='x-show="!mobile"') }}
{{ ui::icon(name="close", size="size-6", attrs='x-cloak x-show="mobile"') }}
</button>
</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
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="/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="/" 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" 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 %}
<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>
<form method="post" action="/admin/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>
<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>
</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>
</li>
{% 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 %}
</ul>
</nav>
@@ -205,5 +171,151 @@
{% block content %}{% endblock content %}
</main>
</div>
<!-- toast notifications: fire from anywhere with toast('message').
Adapted from the vendored Penguin UI component
(penguinui-components/toast-notification/stacking-toast-notification.html):
the docs-only demo trigger buttons are omitted and the malformed quotes on
the upstream dismiss-button <svg> tags are fixed. -->
<div x-data="{
notifications: [],
displayDuration: 8000,
soundEffect: false,
addNotification({ variant = 'info', sender = null, title = null, message = null}) {
const id = Date.now()
const notification = { id, variant, sender, title, message }
if (this.notifications.length >= 20) {
this.notifications.splice(0, this.notifications.length - 19)
}
this.notifications.push(notification)
},
removeNotification(id) {
setTimeout(() => {
this.notifications = this.notifications.filter(
(notification) => notification.id !== id,
)
}, 400);
},
}" x-on:notify.window="addNotification({
variant: $event.detail.variant,
sender: $event.detail.sender,
title: $event.detail.title,
message: $event.detail.message,
})">
<div x-on:mouseenter="$dispatch('pause-auto-dismiss')" x-on:mouseleave="$dispatch('resume-auto-dismiss')" class="group pointer-events-none fixed inset-x-8 top-0 z-99 flex max-w-full flex-col gap-2 bg-transparent px-6 py-6 md:bottom-0 md:left-[unset] md:right-0 md:top-[unset] md:max-w-sm">
<template x-for="(notification, index) in notifications" x-bind:key="notification.id">
<div>
<!-- Info Notification -->
<template x-if="notification.variant === 'info'">
<div x-data="{ isVisible: false, timeout: null }" x-cloak x-show="isVisible" class="pointer-events-auto relative rounded-radius border border-info bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark" role="alert" x-on:pause-auto-dismiss.window="clearTimeout(timeout)" x-on:resume-auto-dismiss.window=" timeout = setTimeout(() => {(isVisible = false), removeNotification(notification.id) }, displayDuration)" x-init="$nextTick(() => { isVisible = true }), (timeout = setTimeout(() => { isVisible = false, removeNotification(notification.id)}, displayDuration))" x-transition:enter="transition duration-300 ease-out" x-transition:enter-end="translate-y-0" x-transition:enter-start="translate-y-8" x-transition:leave="transition duration-300 ease-in" x-transition:leave-end="-translate-x-24 opacity-0 md:translate-x-24" x-transition:leave-start="translate-x-0 opacity-100">
<div class="flex w-full items-center gap-2.5 bg-info/10 rounded-radius p-4 transition-all duration-300">
<div class="rounded-full bg-info/15 p-0.5 text-info" aria-hidden="true">
<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="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">
<h3 x-cloak x-show="notification.title" class="text-sm font-semibold text-info" x-text="notification.title"></h3>
<p x-cloak x-show="notification.message" class="text-pretty text-sm" x-text="notification.message"></p>
</div>
<button type="button" class="ml-auto" aria-label="dismiss notification" x-on:click="(isVisible = false), removeNotification(notification.id)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" class="size-5 shrink-0" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
</template>
<!-- Success Notification -->
<template x-if="notification.variant === 'success'">
<div x-data="{ isVisible: false, timeout: null }" x-cloak x-show="isVisible" class="pointer-events-auto relative rounded-radius border border-success bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark" role="alert" x-on:pause-auto-dismiss.window="clearTimeout(timeout)" x-on:resume-auto-dismiss.window=" timeout = setTimeout(() => {(isVisible = false), removeNotification(notification.id) }, displayDuration)" x-init="$nextTick(() => { isVisible = true }), (timeout = setTimeout(() => { isVisible = false, removeNotification(notification.id)}, displayDuration))" x-transition:enter="transition duration-300 ease-out" x-transition:enter-end="translate-y-0" x-transition:enter-start="translate-y-8" x-transition:leave="transition duration-300 ease-in" x-transition:leave-end="-translate-x-24 opacity-0 md:translate-x-24" x-transition:leave-start="translate-x-0 opacity-100">
<div class="flex w-full items-center gap-2.5 bg-success/10 rounded-radius p-4 transition-all duration-300">
<div class="rounded-full bg-success/15 p-0.5 text-success" aria-hidden="true">
<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="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">
<h3 x-cloak x-show="notification.title" class="text-sm font-semibold text-success" x-text="notification.title"></h3>
<p x-cloak x-show="notification.message" class="text-pretty text-sm" x-text="notification.message"></p>
</div>
<button type="button" class="ml-auto" aria-label="dismiss notification" x-on:click="(isVisible = false), removeNotification(notification.id)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" class="size-5 shrink-0" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
</template>
<!-- Warning Notification -->
<template x-if="notification.variant === 'warning'">
<div x-data="{ isVisible: false, timeout: null }" x-cloak x-show="isVisible" class="pointer-events-auto relative rounded-radius border border-warning bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark" role="alert" x-on:pause-auto-dismiss.window="clearTimeout(timeout)" x-on:resume-auto-dismiss.window=" timeout = setTimeout(() => {(isVisible = false), removeNotification(notification.id) }, displayDuration)" x-init="$nextTick(() => { isVisible = true }), (timeout = setTimeout(() => { isVisible = false, removeNotification(notification.id)}, displayDuration))" x-transition:enter="transition duration-300 ease-out" x-transition:enter-end="translate-y-0" x-transition:enter-start="translate-y-8" x-transition:leave="transition duration-300 ease-in" x-transition:leave-end="-translate-x-24 opacity-0 md:translate-x-24" x-transition:leave-start="translate-x-0 opacity-100">
<div class="flex w-full items-center gap-2.5 bg-warning/10 rounded-radius p-4 transition-all duration-300">
<div class="rounded-full bg-warning/15 p-0.5 text-warning" aria-hidden="true">
<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="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">
<h3 x-cloak x-show="notification.title" class="text-sm font-semibold text-warning" x-text="notification.title"></h3>
<p x-cloak x-show="notification.message" class="text-pretty text-sm" x-text="notification.message"></p>
</div>
<button type="button" class="ml-auto" aria-label="dismiss notification" x-on:click="(isVisible = false), removeNotification(notification.id)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" class="size-5 shrink-0" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
</template>
<!-- Danger Notification -->
<template x-if="notification.variant === 'danger'">
<div x-data="{ isVisible: false, timeout: null }" x-cloak x-show="isVisible" class="pointer-events-auto relative rounded-radius border border-danger bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark" role="alert" x-on:pause-auto-dismiss.window="clearTimeout(timeout)" x-on:resume-auto-dismiss.window=" timeout = setTimeout(() => {(isVisible = false), removeNotification(notification.id) }, displayDuration)" x-init="$nextTick(() => { isVisible = true }), (timeout = setTimeout(() => { isVisible = false, removeNotification(notification.id)}, displayDuration))" x-transition:enter="transition duration-300 ease-out" x-transition:enter-end="translate-y-0" x-transition:enter-start="translate-y-8" x-transition:leave="transition duration-300 ease-in" x-transition:leave-end="-translate-x-24 opacity-0 md:translate-x-24" x-transition:leave-start="translate-x-0 opacity-100">
<div class="flex w-full items-center gap-2.5 bg-danger/10 rounded-radius p-4 transition-all duration-300">
<div class="rounded-full bg-danger/15 p-0.5 text-danger" aria-hidden="true">
<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="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">
<h3 x-cloak x-show="notification.title" class="text-sm font-semibold text-danger" x-text="notification.title"></h3>
<p x-cloak x-show="notification.message" class="text-pretty text-sm" x-text="notification.message"></p>
</div>
<button type="button" class="ml-auto" aria-label="dismiss notification" x-on:click="(isVisible = false), removeNotification(notification.id)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" class="size-5 shrink-0" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
</template>
<!-- Message Notification -->
<template x-if="notification.variant === 'message'">
<div x-data="{ isVisible: false, timeout: null }" x-cloak x-show="isVisible" class="pointer-events-auto relative rounded-radius border border-outline bg-surface text-on-surface dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark" role="alert" x-on:pause-auto-dismiss.window="clearTimeout(timeout)" x-on:resume-auto-dismiss.window="timeout = setTimeout(() => { isVisible = false, removeNotification(notification.id) }, displayDuration)" x-init="$nextTick(() => { isVisible = true }), (timeout = setTimeout(() => { isVisible = false, removeNotification(notification.id) }, displayDuration))" x-transition:enter="transition duration-300 ease-out" x-transition:enter-end="translate-y-0" x-transition:enter-start="translate-y-8" x-transition:leave="transition duration-300 ease-in" x-transition:leave-end="-translate-x-24 opacity-0 md:translate-x-24" x-transition:leave-start="translate-x-0 opacity-100">
<div class="flex w-full rounded-radius items-center gap-2.5 bg-surface-alt p-4 transition-all duration-300 dark:bg-surface-dark-alt">
<div class="flex w-full items-center gap-2.5">
<img x-cloak x-show="notification.sender.avatar" class="mr-2 size-12 rounded-full" alt="avatar" aria-hidden="true" x-bind:src="notification.sender.avatar"/>
<div class="flex flex-col items-start gap-2">
<h3 x-cloak x-show="notification.sender.name" class="text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong" x-text="notification.sender.name"></h3>
<p x-cloak x-show="notification.message" class="text-pretty text-sm" x-text="notification.message"></p>
<div class="flex items-center gap-4">
<button type="button" class="whitespace-nowrap bg-transparent text-center text-sm font-bold tracking-wide text-primary transition hover:opacity-75 active:opacity-100 dark:text-primary-dark">Reply</button>
<button type="button" class="whitespace-nowrap bg-transparent text-center text-sm font-bold tracking-wide text-on-surface transition hover:opacity-75 active:opacity-100 dark:text-on-surface-dark" x-on:click=" (isVisible = false), setTimeout(() => { removeNotification(notification.id) }, 400)">Dismiss</button>
</div>
</div>
</div>
<button type="button" class="ml-auto" aria-label="dismiss notification" x-on:click="(isVisible = false), removeNotification(notification.id)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" class="size-5 shrink-0" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
</body>
</html>

View File

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

215
assets/views/macros/ui.html Normal file
View File

@@ -0,0 +1,215 @@
{# Reusable UI macros adapted from vendored Penguin UI components.
These are OUR adaptation layer; the byte-for-byte upstream sources live under
penguinui-components/. Tailwind sees the full literal class strings here
(assets/css/app.css has @source "../views"), so every branch must spell its
classes out in full — never build class names by concatenation.
Usage:
{% import "macros/ui.html" as ui %}
{{ ui::button(label=t(key="save", lang=lang)) }} {# default primary #}
{{ ui::button(label="Add", attrs='hx-post="/x"' | safe) }}
{{ 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") }}
Notes:
- Macros can't see template context vars (e.g. `lang`); pass already-translated
strings as `label`.
- `attrs` is injected raw (caller must pass it through `| safe`); use it for
htmx / name / value / @click / :disabled etc. For buttons whose attrs carry
nested quotes (e.g. hx-on with toast(...)), keep them inline instead.
- `pad` is the size (default Penguin "px-4 py-2"); override to preserve an
existing size rather than normalizing it.
- 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(label, variant="primary", type="button", href="", attrs="", extra="", icon="", size="px-4 py-2 text-sm") -%}
{%- 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" -%}
{%- 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 %}
{# Icon-only button (square). Penguin ghost treatment (bg-transparent,
hover:opacity-75); pass the raw <svg> as `icon`, an accessible name via
`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 %}
{# Inline icon set — the Heroicons-style SVGs that were duplicated across
base.html / admin/base.html (hamburger, close, cart). Penguin ships no icon
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
are decorative: aria-hidden is baked in — put the accessible name on the
enclosing button/link. The chevron dropdown arrows (checkout, _sidebar) stay
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
penguinui/alert/default-alert.html (danger variant), trimmed to a single line
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="") -%}
<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">
<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>
<span>{{ message }}</span>
</div>
{%- endmacro alert_danger %}
{# Soft-color badge. variant ∈ success | danger | warning | info | primary | neutral #}
{% macro badge(label, variant="neutral") -%}
{% if variant == "success" -%}
<span class="inline-flex w-fit overflow-hidden rounded-radius border border-success bg-surface text-xs font-medium text-success dark:bg-surface-dark"><span class="bg-success/10 px-2 py-1">{{ label }}</span></span>
{%- elif variant == "danger" -%}
<span class="inline-flex w-fit overflow-hidden rounded-radius border border-danger bg-surface text-xs font-medium text-danger dark:bg-surface-dark"><span class="bg-danger/10 px-2 py-1">{{ label }}</span></span>
{%- elif variant == "warning" -%}
<span class="inline-flex w-fit overflow-hidden rounded-radius border border-warning bg-surface text-xs font-medium text-warning dark:bg-surface-dark"><span class="bg-warning/10 px-2 py-1">{{ label }}</span></span>
{%- elif variant == "info" -%}
<span class="inline-flex w-fit overflow-hidden rounded-radius border border-info bg-surface text-xs font-medium text-info dark:bg-surface-dark"><span class="bg-info/10 px-2 py-1">{{ label }}</span></span>
{%- elif variant == "primary" -%}
<span class="inline-flex w-fit 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="bg-primary/10 px-2 py-1 dark:bg-primary-dark/10">{{ label }}</span></span>
{%- else -%}
<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 %}
{%- 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

@@ -0,0 +1,56 @@
{# Settings dropdown (language + theme). Shared by base.html and admin/base.html
to kill the former ~100-line copy-paste duplication.
Adapted from the vendored Penguin UI component
penguinui-components/dropdowns/dropdown-with-click.html: Penguin's dropdown
menu container + item treatment. Deviations: kept our gear icon-only trigger
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
bg-primary/5 to stay consistent with the rest of our Penguin-ified UI.
The host template provides the wrapper
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative ...">
so it controls its own positioning (e.g. ml-auto in admin). #}
{{ 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>') }}
<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"
role="menu">
<form method="post" action="/lang" hx-boost="false">
<p class="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-language", lang=lang | default(value='sk')) }}
</p>
<button type="submit" name="lang" value="en" 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">
<span>English</span>
{% if lang | default(value='sk') == "en" %}<span class="text-primary dark:text-primary-dark"></span>{% endif %}
</button>
<button type="submit" name="lang" value="sk" 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">
<span>Slovenčina</span>
{% if lang | default(value='sk') == "sk" %}<span class="text-primary dark:text-primary-dark"></span>{% endif %}
</button>
</form>
<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')) }}
</p>
{# Theme picker: Penguin UI's switch toggle (penguinui-components/toggle/default-toggle.html),
adapted from a standalone checkbox into a light/dark switch wired to our existing
setTheme()/currentTheme() helpers in base.html (which drive <html data-theme>). The `dark`
state initialises from the resolved theme — so a stored 'system' preference still reflects
the current appearance — and stays in sync via the theme:changed event the helpers dispatch.
Toggling writes an explicit light/dark choice (the old tri-state 'system' option is dropped,
matching Penguin's binary toggle). #}
<div class="px-4 py-2"
x-data="{
labels: { light: '{{ t(key='theme-light', lang=lang | default(value='sk')) }}', dark: '{{ t(key='theme-dark', lang=lang | default(value='sk')) }}' },
dark: document.documentElement.getAttribute('data-theme') === 'dark'
}"
@theme:changed.document="dark = document.documentElement.getAttribute('data-theme') === 'dark'">
<label for="themeToggle" class="inline-flex w-full cursor-pointer items-center justify-between gap-3">
<span class="text-sm font-medium text-on-surface dark:text-on-surface-dark" x-text="dark ? labels.dark : labels.light"></span>
<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>

View File

@@ -1,29 +1,37 @@
<div
class="group flex flex-col overflow-hidden rounded-radius border border-outline bg-surface transition hover:border-primary dark:border-outline-dark dark:bg-surface-dark-alt dark:hover:border-primary-dark">
{# Adapted from the vendored Penguin UI component
(penguinui-components/card/ecommerce-product-card.html):
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
the shop grid) are dropped; the whole card links to the product page. #}
<article
class="group flex flex-col overflow-hidden rounded-radius border border-outline bg-surface-alt text-on-surface transition hover:border-primary dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark dark:hover:border-primary-dark">
<a href="/shop/{{ product.slug }}" class="flex flex-1 flex-col">
<div class="aspect-square overflow-hidden bg-surface-alt dark:bg-surface-dark">
<!-- Image -->
<div class="h-44 overflow-hidden bg-surface-alt md:h-64 dark:bg-surface-dark">
{% if product.image %}
<img src="/images/{{ product.image }}" alt="{{ product.name }}" class="size-full object-cover transition group-hover:scale-105">
<img src="/images/{{ product.image }}" alt="{{ product.name }}" class="size-full object-cover transition duration-700 ease-out group-hover:scale-105">
{% endif %}
</div>
<div class="flex flex-1 flex-col gap-1 p-4 pb-2">
<h3 class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ product.name }}</h3>
<p class="mt-auto pt-2 font-semibold text-primary dark:text-primary-dark">{{ product.price }} {{ product.currency }}</p>
<!-- Content -->
<div class="flex flex-1 flex-col gap-1 p-6 pb-2">
<!-- Header: Title & Price -->
<div class="flex justify-between gap-4">
<h3 class="text-lg font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ product.name }}</h3>
<span class="whitespace-nowrap text-xl"><span class="sr-only">Price</span>{{ product.price }} {{ product.currency }}</span>
</div>
</div>
</a>
<div class="flex flex-col gap-2 px-4 pb-4">
<div class="flex flex-col gap-2 p-6 pt-0">
{% if product.stock > 0 %}
<p class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="in-stock", lang=lang | default(value='sk')) }}: {{ product.stock }}</p>
<form method="post" action="/cart/add" hx-boost="false">
<form method="post" action="/cart/add" hx-post="/cart/add" hx-swap="none"
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="quantity" value="1">
<button type="submit"
class="inline-flex w-full items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="add-to-cart", lang=lang | default(value='sk')) }}
</button>
{{ 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>') }}
</form>
{% 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>
{% endif %}
</div>
</div>
</article>

View File

@@ -1,19 +1,20 @@
{# Cart contents, swapped in via htmx on quantity change / removal so the page
never does a full reload. Rendered inside <div id="cart-body"> in cart.html
and returned on its own by /cart/update and /cart/remove. #}
{% import "macros/ui.html" as ui %}
{% if items | length > 0 %}
<div class="overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
<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 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
<div class="{{ ui::table_wrap_cls() }}">
<table class="{{ ui::table_cls() }}">
<thead class="{{ ui::thead_cls() }}">
<tr>
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 font-semibold">{{ t(key="price", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 font-semibold">{{ t(key="quantity", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3 text-right font-semibold">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</th>
<th class="px-4 py-3"></th>
{{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="price", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="quantity", lang=lang | default(value='sk'))) }}
{{ ui::th(label=t(key="cart-total", lang=lang | default(value='sk')), align="text-right") }}
{{ ui::th(label="") }}
</tr>
</thead>
<tbody class="divide-y divide-outline dark:divide-outline-dark">
<tbody class="{{ ui::tbody_cls() }}">
{% for item in items %}
<tr>
<td class="px-4 py-3">
@@ -35,7 +36,7 @@
$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>
</td>
<td class="px-4 py-3 text-right font-medium tabular-nums">{{ item.line_total }} {{ item.currency }}</td>
@@ -43,13 +44,13 @@
<form method="post" action="/cart/remove"
hx-post="/cart/remove" hx-target="#cart-body" hx-swap="innerHTML">
<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>
</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="border-t border-outline dark:border-outline-dark">
<tfoot class="{{ ui::tfoot_cls() }}">
<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 class="px-4 py-3 text-right text-lg font-bold tabular-nums text-primary dark:text-primary-dark">{{ total }} {{ currency }}</td>
@@ -60,12 +61,12 @@
</div>
<div class="mt-6 flex flex-wrap justify-between gap-3">
<a href="/shop" class="inline-flex items-center rounded-radius border border-outline px-4 py-2 text-sm 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="cart-continue", lang=lang | default(value='sk')) }}</a>
<a href="/checkout" class="inline-flex items-center justify-center rounded-radius bg-primary px-5 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="cart-checkout", lang=lang | default(value='sk')) }}</a>
{{ ui::button(variant="outline-secondary", label=t(key="cart-continue", lang=lang | default(value='sk')), href="/shop") }}
{{ ui::button(label=t(key="cart-checkout", lang=lang | default(value='sk')), href="/checkout", size="px-5 py-2 text-sm") }}
</div>
{% else %}
<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>
<a href="/shop" class="mt-4 inline-flex items-center justify-center rounded-radius bg-primary px-4 py-2 text-sm font-medium text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">{{ t(key="cart-continue", lang=lang | default(value='sk')) }}</a>
{{ ui::button(label=t(key="cart-continue", lang=lang | default(value='sk')), href="/shop", extra="mt-4") }}
</div>
{% endif %}

View File

@@ -4,57 +4,59 @@
with children is expandable (accordion); one without is a plain link.
Active state is set client-side by markActiveNav() via data-nav +
aria-current; groups auto-expand when the current page is the category or
one of its subcategories. #}
<p class="px-3 pb-2 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
one of its subcategories.
Adapted from the vendored Penguin UI component
penguinui-components/sidebar/sidebar-with-collapsible-menus.html: Penguin's
link treatment + active state + chevron-down rotation. Deviations: the group
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
(that Alpine plugin isn't bundled in our build). #}
<p class="px-2 pb-2 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
{{ t(key="categories", lang=lang | default(value='sk')) }}
</p>
<ul class="flex flex-col gap-0.5">
<li>
<a href="/shop" data-nav="/shop"
class="block rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
{{ t(key="all-products", lang=lang | default(value='sk')) }}
</a>
</li>
<div class="flex flex-col gap-1">
<a href="/shop" data-nav="/shop"
class="flex items-center gap-2 rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ t(key="all-products", lang=lang | default(value='sk')) }}
</a>
{% for group in category_groups %}
{% if group.children | length > 0 %}
<li x-data="{ open: false }"
<div x-data="{ open: false }" class="flex flex-col"
x-init="open = ['{{ group.slug }}'{% for child in group.children %}, '{{ child.slug }}'{% endfor %}].some(s => location.pathname === '/category/' + s)">
<div class="flex items-stretch">
<a href="/category/{{ group.slug }}" data-nav="/category/{{ group.slug }}"
class="flex-1 truncate rounded-l-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
class="flex flex-1 items-center gap-2 truncate rounded-l-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ group.name }}
</a>
<button type="button" @click="open = !open" :aria-expanded="open"
<button type="button" x-on:click="open = ! open" x-bind:aria-expanded="open ? 'true' : 'false'"
aria-label="{{ group.name }}"
class="inline-flex w-8 shrink-0 items-center justify-center rounded-r-radius text-on-surface/60 transition hover:bg-surface hover:text-primary dark:text-on-surface-dark/60 dark:hover:bg-surface-dark dark:hover:text-primary-dark">
<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="open && 'rotate-90'">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
class="inline-flex w-8 shrink-0 items-center justify-center rounded-r-radius text-on-surface/60 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline dark:text-on-surface-dark/60 dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
class="size-5 shrink-0 transition-transform rotate-0" x-bind:class="open ? 'rotate-180' : 'rotate-0'" aria-hidden="true">
<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>
</button>
</div>
<ul x-show="open" x-cloak x-transition class="mt-0.5 flex flex-col gap-0.5">
<ul x-show="open" x-cloak x-transition class="ml-3 mt-0.5 flex flex-col gap-0.5 border-l border-outline pl-1 dark:border-outline-dark">
{% for child in group.children %}
<li>
<a href="/category/{{ child.slug }}" data-nav="/category/{{ child.slug }}" style="padding-left: 28px"
class="flex items-center gap-1.5 rounded-radius py-1.5 pr-3 text-sm text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
<span class="text-on-surface/40 dark:text-on-surface-dark/40"></span>
<span class="truncate">{{ child.name }}</span>
<a href="/category/{{ child.slug }}" data-nav="/category/{{ child.slug }}"
class="flex items-center gap-2 truncate rounded-radius px-2 py-1.5 text-sm text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ child.name }}
</a>
</li>
{% endfor %}
</ul>
</li>
</div>
{% else %}
<li>
<a href="/category/{{ group.slug }}" data-nav="/category/{{ group.slug }}"
class="block truncate rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
{{ group.name }}
</a>
</li>
<a href="/category/{{ group.slug }}" data-nav="/category/{{ group.slug }}"
class="flex items-center gap-2 truncate rounded-radius px-2 py-1.5 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-on-surface-strong focus:outline-hidden focus-visible:underline aria-[current=page]:bg-primary/10 aria-[current=page]:text-on-surface-strong dark:text-on-surface-dark dark:hover:bg-primary-dark/5 dark:hover:text-on-surface-dark-strong dark:aria-[current=page]:bg-primary-dark/10 dark:aria-[current=page]:text-on-surface-dark-strong">
{{ group.name }}
</a>
{% endif %}
{% endfor %}
</ul>
</div>
{% if category_groups | length == 0 %}
<p class="px-3 py-2 text-sm text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="shop-empty", lang=lang | default(value='sk')) }}</p>
<p class="px-2 py-2 text-sm text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="shop-empty", lang=lang | default(value='sk')) }}</p>
{% endif %}

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="checkout-title", lang=lang | default(value='sk')) }}{% endblock title %}
@@ -10,6 +11,7 @@
<form method="post" action="/checkout" hx-boost="false"
x-data="{
paymentMethod: '',
accountType: '{{ prefill_account_type | default(value='personal') }}',
carrier: '',
carrierPrice: 0,
requiresPoint: false,
@@ -30,25 +32,73 @@
class="mt-6 grid gap-8 lg:grid-cols-3">
<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 -->
<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="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>
<input id="email" name="email" type="email" required
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">
<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>
{{ ui::input(name="email", id="email", type="email", value=prefill_email | default(value=''), required=true, autocomplete="email") }}
</div>
<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>
<input id="customer_name" name="customer_name" type="text" required
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">
<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>
{{ ui::input(name="customer_name", id="customer_name", value=prefill_name | default(value=''), required=true, autocomplete="name") }}
</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>
<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">
<!-- 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: '+421', opts: [
x-data="{ prefixOpen: false, prefix: '{{ prefill_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' },
@@ -72,8 +122,7 @@
</template>
</ul>
</div>
<input id="phone" name="phone" type="tel" required autocomplete="tel" inputmode="tel" placeholder="900 000 000"
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">
{{ ui::input(name="phone", id="phone", type="tel", value=prefill_phone | default(value=''), required=true, autocomplete="tel", placeholder="900 000 000", attrs='inputmode="tel"') }}
</div>
</div>
</fieldset>
@@ -82,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">
<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>
<input id="address" name="address" type="text" required
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">
<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>
{{ ui::input(name="address", id="address", value=prefill_address | default(value=''), required=true, 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>
<input id="city" name="city" type="text" required
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">
<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>
{{ ui::input(name="city", id="city", value=prefill_city | default(value=''), required=true, 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>
<input id="zip" name="zip" type="text" required
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">
<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>
{{ ui::input(name="zip", id="zip", value=prefill_zip | default(value=''), required=true, 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>
<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"
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-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')) }}' },
@@ -131,13 +177,14 @@
<!-- carrier -->
<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 %}
<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">
<!-- 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
@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>
<span class="tabular-nums text-on-surface/80 dark:text-on-surface-dark/80">{{ m.price }} {{ currency }}</span>
@@ -166,24 +213,34 @@
<!-- payment -->
<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">
<input type="radio" name="payment_method" value="cod" required x-model="paymentMethod"
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark">
{{ ui::radio(name="payment_method", value="cod", attrs='required x-model="paymentMethod"') }}
<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 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"
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark">
{{ ui::radio(name="payment_method", value="bank_transfer", attrs='required x-model="paymentMethod"') }}
<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>
</fieldset>
<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>
<textarea id="note" name="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>
{{ ui::textarea(name="note", id="note", rows="3") }}
</div>
{% if logged_in_customer %}
<!-- logged-in customers can persist this address to their profile for next time -->
{{ ui::checkbox(name="save_profile", id="save_profile", label=t(key="checkout-save-profile", lang=lang | default(value='sk')), checked=true) }}
{% 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>
<!-- summary -->
@@ -211,10 +268,7 @@
<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>
</div>
<button type="submit" :disabled="!canSubmit"
class="inline-flex w-full items-center justify-center rounded-radius bg-primary px-6 py-2.5 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="checkout-place-order", lang=lang | default(value='sk')) }}
</button>
{{ 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>
</form>
{% endblock content %}

View File

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

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ t(key="order-confirmed-title", lang=lang | default(value='sk')) }}{% endblock title %}
@@ -14,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>
</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="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>
@@ -55,7 +62,7 @@
{% endif %}
<div class="text-center">
<a href="/shop" class="inline-flex items-center justify-center rounded-radius border border-outline px-5 py-2 text-sm 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="cart-continue", lang=lang | default(value='sk')) }}</a>
{{ 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>
{% endblock content %}

View File

@@ -1,17 +1,39 @@
{% extends "base.html" %}
{% import "macros/ui.html" as ui %}
{% block title %}{{ product.name }}{% endblock title %}
{% block content %}
<div class="grid gap-10 lg:grid-cols-2">
<!-- gallery -->
<div x-data="{ active: 0 }" class="space-y-4">
<div class="aspect-square overflow-hidden rounded-radius border border-outline bg-surface-alt dark:border-outline-dark dark:bg-surface-dark-alt">
<!-- gallery — prev/next arrows + opacity transitions adapted from
penguinui/carousel/default-carousel.html; kept our product thumbnail strip
(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 %}
{% 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 %}
{% 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>
{% if images | length > 1 %}
<div class="flex flex-wrap gap-2">
@@ -39,17 +61,14 @@
{% endif %}
{% if product.stock > 0 %}
<form method="post" action="/cart/add" hx-boost="false" class="flex flex-wrap items-end gap-3">
<form method="post" action="/cart/add" hx-post="/cart/add" hx-swap="none" class="flex flex-wrap items-end gap-3"
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 }}">
<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>
<input id="quantity" name="quantity" type="number" min="1" max="{{ product.stock }}" value="1"
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">
{{ ui::input(name="quantity", id="quantity", type="number", value="1", width="w-24", attrs='min="1" max="' ~ product.stock ~ '"') }}
</div>
<button type="submit"
class="inline-flex items-center justify-center rounded-radius bg-primary px-5 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 dark:bg-primary-dark dark:text-on-primary-dark">
{{ t(key="add-to-cart", lang=lang | default(value='sk')) }}
</button>
{{ ui::button(label=t(key="add-to-cart", lang=lang | default(value='sk')), type="submit", size="px-5 py-2 text-sm") }}
</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>
{% 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:
# 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:
# Enable/Disable smtp mailer.
enable: true
enable: {{ get_env(name="SMTP_ENABLE", default="true") }}
# SMTP server host. e.x localhost, smtp.gmail.com
host: localhost
host: "{{ get_env(name="SMTP_HOST", default="localhost") }}"
# SMTP server port
port: 1025
port: {{ get_env(name="SMTP_PORT", default="1025") }}
# Use secure connection (SSL/TLS).
secure: false
# auth:
# user:
# password:
secure: {{ get_env(name="SMTP_SECURE", default="false") }}
{% if get_env(name="SMTP_PASSWORD", default="") != "" %}
auth:
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)
# hello_name:
@@ -125,3 +130,30 @@ settings:
# Bank-transfer payment details shown on the order confirmation.
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.") }}
# 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:
admin_email: "{{ get_env(name="ADMIN_EMAIL", default="") }}"
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

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

637
hardcoded-inventory.md Normal file
View File

@@ -0,0 +1,637 @@
# Handcoded UI Components — Penguin UI Replacement Index
> **Scope**: Every handcoded UI component.
> Each item maps to a [Penguin UI](https://github.com/SalarHoushvand/penguinui-components/tree/main) component that duplicates the same purpose with fewer lines and better accessibility.
## Vendoring convention
The full library is now vendored locally at repo-root
`penguinui-components/` (177 component `.html` files, moved there
2026-06-18). Read components from there — **NO** network/curl/WebFetch
needed anymore.
### HARD RULE — read-only, never edit
`penguinui-components/` is a read-only third-party library, **NOT our
code**. Never edit, never `{% include %}`, never adapt in place. It is
reference only; **copy markup OUT** of it and adapt at the use-site.
When a Penguin UI component can replace a handcoded one:
1. Find the component in the local `penguinui-components/` directory.
2. Copy its markup **out** into the appropriate `assets/views/` location,
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.
---
## 0. Toast — ✅ DONE
**Penguin UI: `toast-notification/stacking-toast-notification.html`**
- 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
removed; the upstream dismiss-button `<svg>` quote bugs fixed)
- The global `toast('message')` JS helper now dispatches the component's
`notify` event (`{ variant: 'success', message }`), so existing callsites
(`shop/show.html`, `shop/_card.html`) keep working unchanged.
---
## 1. Navbar — ✅ DONE
**Penguin UI: `navbar/default-navbar.html`**
- Exact upstream mirror at `penguinui-components/navbar/default-navbar.html` (reference only)
- **Link treatment** adopted from Penguin (matches the already-ported sidebars): the
desktop nav links lost the pill-hover (`hover:bg-surface-alt` + `px-3 py-1.5`) for
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`).
| # | Location | What it is |
|---|----------|------------|
| 1 | `assets/views/base.html` | Full site navbar (brand, links, cart badge, settings, mobile menu) |
| 2 | `assets/views/admin/base.html` | Admin top bar: animated hamburger + breadcrumb + settings |
---
## 2. Sidebar (Admin) — ✅ DONE
**Penguin UI: `sidebar/simple-sidebar.html`**
- 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
exit/logout now use Penguin's link treatment (`hover:bg-primary/5`,
`underline-offset-2 focus-visible:underline focus:outline-hidden`) and the
subtle active state (`bg-primary/10` + `text-on-surface-strong`) mapped onto
our `data-nav`/`aria-current` so `markActiveNav()` still drives it.
- The fixed-rail translate-X show/hide mechanics + mobile overlay (#4) are layout
scaffolding, kept as-is. Icons were intentionally not added (no verified icon
set yet) — possible follow-up.
---
## 3. Sidebar (Category Accordion) — ✅ DONE
**Penguin UI: `sidebar/sidebar-with-collapsible-menus.html`**
- 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 +
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
htmx partial, data-driven `category_groups`, auto-expand `x-init`, and
`data-nav`/`markActiveNav()` active routing.
- Deviations: group row keeps our link + chevron-toggle split (categories are
navigable, not just expandable); uses `x-show`/`x-transition` instead of
upstream's `x-collapse` (that Alpine plugin isn't bundled in our build).
- The `<aside>` drawer + mobile overlay (#6) in `base.html` are layout
scaffolding, kept as-is.
---
## 4. Dropdown (Settings) — ✅ DONE
**Penguin UI: `dropdowns/dropdown-with-click.html`**
- 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
`assets/views/partials/settings_dropdown.html`, included by both `base.html`
and `admin/base.html` (each host keeps its own positioning wrapper
`<div x-data="{ open:false }" class="relative [ml-auto]">`).
- Adopts Penguin's dropdown menu container + item treatment. Deviations: kept our
gear icon-only trigger and core-Alpine open/@click.outside toggle (upstream's
`x-trap`/`$focus` need the Alpine Focus plugin we don't bundle); item hover
uses `bg-primary/5` for consistency with the rest of the UI.
---
## 5. Country / Phone Combobox — ⛔ WON'T PORT (conscious deviation)
**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 |
|---|----------|------------|------|
| 9 | `assets/views/shop/checkout.html:49-74` | Phone prefix combobox (`+421`, `+420`, …, `+33`) | ~25 lines |
| 10 | `assets/views/shop/checkout.html:102-127` | Country combobox (SK, CZ, AT, DE, PL, HU) | ~26 lines |
**Details for #9:**
- Alpine `x-data` with `prefix`, `prefixOpen`, `opts` array of `{ v, l }` (9 country codes)
- Manual `filtered` computed property
- Inline chevron SVG that rotates via `:class="prefixOpen && 'rotate-180'"`
- Dropdown list with `<template x-for>` and `@click` selection
**Details for #10:**
- Same pattern as #9 but with translate-able country names (6 countries)
- Includes `+421` prefix shortcut
---
## 6. Product Card — ✅ DONE
**Penguin UI: `card/ecommerce-product-card.html`**
- 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
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,
hardcoded content and `max-w-sm` (fights the shop grid) were dropped; whole card
links to the product page; out-of-stock badge kept.
---
## 7. Product Image Gallery — ✅ DONE
**Penguin UI: `carousel/default-carousel.html`**
- 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
prev/next arrow buttons (`bg-surface/40` rounded, verbatim chevron SVGs) and
`x-transition.opacity.duration.300ms` fade between images. Added `prev()`/`next()`
with wraparound to the gallery `x-data`; arrows + transitions only render when
`images | length > 1`.
- Deviations: kept our **product thumbnail strip** (more useful than carousel
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 — ✅ DONE
**Penguin UI: `radio/radio-with-container.html`**
- 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`
emits **only** the Penguin custom radio-dot `<input>` (verbatim `appearance-none`
+ `before:` dot + `checked:bg-primary`). Callers keep their own card-style
`<label>` wrapper — we kept our `has-[:checked]:border-primary` card highlight,
which is richer than Penguin's plain `bg-surface-alt` container.
- Adopted at `shop/checkout.html`: both **payment** radios (`ui::radio` with
`attrs='required x-model="paymentMethod"'`). The **carrier** radio (in the
`{% for m in shipping_methods %}` loop) keeps the same Penguin dot class **inline**
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
qty input). Native `text-primary` radios are gone.
---
## 9. Checkbox — ✅ DONE
**Penguin UI: `checkbox/default-checkbox.html`**
- 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`
(full Penguin control: custom box + check-icon + label, `has-checked:`/`peer` variants).
- Adopted: product/category "Published" + shipping "Enabled".
## 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.
---
## 14. Table — ✅ DONE
**Penguin UI: `table/default-table.html`**
- 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
5 tables (orders index, order detail, products, categories, cart body). Centralized
into **class-string macros** in `macros/ui.html`: `ui::table_wrap_cls()`,
`ui::table_cls()`, `ui::thead_cls()`, `ui::tbody_cls()`, `ui::row_cls()` (hover),
`ui::tfoot_cls()`, plus an element macro `ui::th(label, align="")` for header cells.
- **Why class-string macros, not full row macros:** Tera has no slot/`{% raw %}{% call %}{% endraw %}`
mechanism, and the cells are heterogeneous (product image+name, htmx quantity input
with inline Alpine `@change`, badges, action-button forms), so rows stay inline. The
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()`.
| # | Location | What it is |
|---|----------|------------|
| 34 | `assets/views/admin/orders/index.html` | Orders table |
| 35 | `assets/views/admin/orders/show.html` | Order items table + tfoot summary |
| 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) |
| 38 | `assets/views/shop/_cart_body.html` | Cart table (htmx qty input, remove) + tfoot total |
---
## 15. Alert / Error Banner — ✅ DONE
**Penguin UI: `alert/default-alert.html`**
- Exact upstream mirror at `penguinui-components/alert/default-alert.html` (reference only)
- Adapted into the `ui::alert_danger(message, extra="")` macro in
`assets/views/macros/ui.html` (compact one-line danger alert + danger icon).
- Adopted at both sites: `admin/login.html` (login error) and
`admin/orders/show.html` (ship error).
---
## 16. Badge / Status Pill — ✅ DONE
**Penguin UI: `badge/soft-color-badge.html`**
- 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`
(variants: success | danger | warning | info | primary | neutral).
- Adopted at the status-pill sites: "Auth" badge (`admin/login.html`), order status
(`orders/index.html`, neutral), Published/Draft pills (`products.html` +
`categories.html`, success/neutral).
- Intentionally left inline (not soft-color pills): the cart item-count **notification**
badge in `base.html` (count bubble, a different Penguin badge type) and the
block-style "out of stock" notice in `_card.html`.
---
## 17. Buttons — ✅ DONE
**Penguin UI: `buttons/default-button.html`, `outline-button.html`, `ghost-button.html`, `button-with-icon.html`**
- Exact upstream mirrors at `penguinui-components/buttons/*.html` (reference only).
- Macros in `assets/views/macros/ui.html`:
`ui::button(label, variant="primary", type, href, attrs, extra, icon, size="px-4 py-2 text-sm")`
and `ui::icon_button(icon, variant="ghost-secondary", aria_label, attrs, …)`.
The per-variant class strings are the **verbatim** Penguin variants (solid
`primary|secondary|danger|success|warning|info`, `outline-*`, `ghost-*`) — only
`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 &
category forms (save / cancel = `outline-secondary`), products/categories "new" +
empty-state CTAs, orders detail (back/ship/status), shipping save, cart
(continue/checkout/empty), checkout place-order (`:disabled` via `attrs`),
product detail add-to-cart, order-confirmed continue.
- Icon-only buttons now use `ui::icon_button(icon, variant="ghost-secondary",
aria_label, attrs, …)` — Penguin ghost treatment, square. Converted: settings
gear, both hamburgers (site + admin), admin sidebar toggle, mobile category
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**
> macro scope, so a template that includes a partial which calls `ui::` must also
> `{% import "macros/ui.html" as ui %}` itself (see `shop/cart.html` →
> `shop/_cart_body.html`). In an `{% extends %}` child the import must sit
> directly after `{% extends %}` with no comment/content before it.
---
## 18. Toggle / Switch — LOW PRIORITY (de-duplication, not replacement)
**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 |
|---|----------|------------|------|
| 53 | `assets/views/base.html:13-30` | Theme toggle (dark/light/system) — inline `<script>` JavaScript | ~18 lines |
| 54 | `assets/views/admin/base.html:13-30` | **Exact duplicate** of the theme toggle JS | ~18 lines |
**Details:**
- `applyTheme()`, `setTheme()`, `currentTheme()` — reads/writes `localStorage`
- `matchMedia('prefers-color-scheme: dark')` listener
- All hand-written vanilla JS, duplicated twice (36 lines total)
---
## 19. Inline SVG Icons — MOSTLY DONE
**Penguin UI: none (Penguin uses Heroicons-equivalent inline SVGs)**
> **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
> `ui::icon(name, size, extra, attrs)` macro (`macros/ui.html`); call sites use
> `{{ ui::icon(name="cart") }}` etc. The chevron dropdown arrows stay inline by
> design — they carry nested-quote Alpine `:class` / `x-bind:class` bindings,
> which Tera macro args can't pass cleanly (see the attrs note atop `ui.html`).
| # | 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 — LOW PRIORITY
**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 |
|---|----------|------------|------|
| 63 | `assets/views/admin/orders/index.html:38-39` | "No orders" message | ~2 lines |
| 64 | `assets/views/admin/catalog/products.html:72-78` | "No products" with CTA button | ~7 lines |
| 65 | `assets/views/admin/catalog/categories.html:61-67` | "No categories" with CTA button | ~7 lines |
| 66 | `assets/views/shop/_cart_body.html:67-70` | "Cart empty" with CTA button | ~4 lines |
| 67 | `assets/views/shop/_sidebar.html:58-59` | "No categories" message | ~2 lines |
---
## 21. Dashboard Navigation Cards — LOW PRIORITY
**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 |
|---|----------|------------|------|
| 68 | `assets/views/admin/index.html:12-27` | 3 dashboard link cards (Products, Categories, Orders) | ~16 lines |
**Details:**
- Each card is an `<a>` styled with border, hover effect, and nested title+description
- Same hover pattern: `hover:border-primary`
---
## 22. Checkout Order Summary — LOW PRIORITY
**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 |
|---|----------|------------|------|
| 69 | `assets/views/shop/checkout.html:190-218` | Cart summary aside: item list, subtotal, shipping, total, place-order button | ~29 lines |
**Details:**
- Item list with name × quantity + line total
- Subtotal + shipping + total with `tabular-nums`
- Dynamic shipping price from Alpine `carrierPrice`
- Disabled submit button when `!canSubmit`
---
## 23. Login Card — LOW PRIORITY
**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 |
|---|----------|------------|------|
| 70 | `assets/views/admin/login.html:6-61` | Full login form: header with auth badge, email + password inputs, error alert, submit button | ~56 lines |
---
## 24. Checkout Fieldset Cards — LOW PRIORITY
**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 |
|---|----------|------------|------|
| 71 | `assets/views/shop/checkout.html:34-79` | Contact info fieldset (email, name, phone+prefix) | ~46 lines |
| 72 | `assets/views/shop/checkout.html:82-130` | Shipping address fieldset (address, city, zip, country) | ~49 lines |
| 73 | `assets/views/shop/checkout.html:133-165` | Carrier selection fieldset | ~33 lines |
| 74 | `assets/views/shop/checkout.html:167-180` | Payment method fieldset | ~14 lines |
Each fieldset uses `<fieldset>` + `<legend>` with the same `rounded-radius border border-outline bg-surface p-6` styling.
---
## 25. Order Detail Info Panel — LOW PRIORITY
**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 |
|---|----------|------------|------|
| 75 | `assets/views/admin/orders/show.html:49-77` | Customer + shipping + payment info panel | ~29 lines |
| 76 | `assets/views/admin/orders/show.html:79-103` | Fulfillment panel (tracking, label link, ship button) | ~25 lines |
| 77 | `assets/views/admin/orders/show.html:106-115` | Status update form panel | ~10 lines |
---
## 26. Shipping Method Settings Row — LOW PRIORITY
**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 |
|---|----------|------------|------|
| 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 — LOW PRIORITY
**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 |
|---|----------|------------|------|
| 79 | `assets/views/admin/catalog/product_form.html:15-99` | Full product edit/create form with all fields | ~84 lines |
| 80 | `assets/views/admin/catalog/category_form.html:15-81` | Full category edit/create form with all fields | ~66 lines |
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
| # | Component | Penguin UI Directory | Status | Lines |
|---|-----------|---------------------|--------|-------|
| 0 | Toast | `toast-notification/` | ✅ DONE | — |
| 1 | Navbar | `navbar/` | ✅ DONE | ~143 |
| 2 | Sidebar (admin) | `sidebar/` | ✅ DONE | ~46 |
| 3 | Sidebar (category accordion) | `sidebar/` | ✅ DONE | ~62 |
| 4 | Dropdown (settings) | `dropdowns/` | ✅ DONE | ~103 |
| 5 | Country/Phone combobox | `combobox/` | ⛔ WON'T PORT | ~51 |
| 6 | Product card | `card/` | ✅ DONE | ~30 |
| 7 | Image gallery | `carousel/` | ✅ DONE | ~19 |
| 8 | Radio groups | `radio/` | ✅ DONE | ~47 |
| 9 | Checkbox | `checkbox/` | ✅ DONE | ~15 |
| 10 | Text input | `text-input/` | ✅ DONE | ~146 |
| 11 | Textarea | `text-area/` | ✅ DONE | ~10 |
| 12 | Select | `select/` | ✅ DONE | ~23 |
| 13 | File input | `file-input/` | ✅ DONE | ~12 |
| 14 | Table | `table/` | ✅ DONE | ~196 |
| 15 | Alert/Error | `alert/` | ✅ DONE | ~9 |
| 16 | Badge/Pill | `badge/` | ✅ DONE | ~17 |
| 17 | Button | `buttons/` | ✅ DONE | ~200+ |
| 18 | Toggle (theme) | `toggle/` | LOW (dedup) | ~36 |
| 19 | Inline SVG icons | N/A | LOW | ~50 |
| 20 | Empty state | N/A | LOW | ~22 |
| 21 | Dashboard cards | `card/` | LOW | ~16 |
| 22 | Checkout summary | `card/` | LOW | ~29 |
| 23 | Login card | `card/` | LOW | ~56 |
| 24 | Checkout fieldsets | `card/` | LOW | ~142 |
| 25 | Order info panels | `card/` | LOW | ~64 |
| 26 | Shipping settings row | `card/` | LOW | ~21 |
| 27 | Form wrappers | `card/` | LOW | ~150 |
**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
remaining Phase-3 items (#1827) are already internally Penguin-adapted or have
no applicable match — the migration is effectively complete.**

View File

@@ -13,7 +13,7 @@ loco-rs = { workspace = true }
[dependencies.sea-orm-migration]
version = "1.1.0"
version = "1.1.20"
features = [
# 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 File

@@ -30,6 +30,10 @@ mod m20260616_160000_add_parent_to_categories;
mod m20260617_000001_add_carrier_to_shipping_methods;
mod m20260617_000002_add_shipment_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;
#[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_000002_add_shipment_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)
]
}

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,101 @@
<!-- info Alert -->
<div class="relative w-full overflow-hidden rounded-sm 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="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 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="size-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- success Alert -->
<div class="relative w-full overflow-hidden rounded-sm 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="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 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="size-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- warning Alert -->
<div class="relative w-full overflow-hidden rounded-sm 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="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 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="size-4 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- danger Alert -->
<div class="relative w-full overflow-hidden rounded-sm 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="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 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="size-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,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,40 @@
<!-- 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="px-2 py-1 bg-surface-alt/10 dark:bg-surface-dark-alt/10">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="px-2 py-1 bg-surface-dark-alt/10 dark:bg-surface-alt/10">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="px-2 py-1 bg-primary/10 dark:bg-primary-dark/10">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="px-2 py-1 bg-secondary/10 dark:bg-secondary-dark/10">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="px-2 py-1 bg-info/10 dark:bg-info/10">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="px-2 py-1 bg-success/10 dark:bg-success/10">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="px-2 py-1 bg-warning/10 dark:bg-warning/10">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="px-2 py-1 bg-danger/10 dark:bg-danger/10">Bagde</span>
</span>

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,64 @@
<!-- primary Button with Icon -->
<button type="button" class="inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-radius bg-primary border border-primary dark:border-primary-dark px-4 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 text-center focus-visible:outline 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: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>
Create
</button>
<!-- secondary Button with Icon -->
<button type="button" class="inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-radius bg-secondary border border-secondary dark:border-secondary-dark px-4 py-2 text-sm font-medium tracking-wide text-on-secondary transition hover:opacity-75 text-center focus-visible:outline 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: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>
Create
</button>
<!-- alternate Button with Icon -->
<button type="button" class="inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-radius bg-surface-alt border border-surface-alt dark:border-surface-dark-alt px-4 py-2 text-sm font-medium tracking-wide text-on-surface-strong transition hover:opacity-75 text-center focus-visible:outline 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: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>
Create
</button>
<!-- inverse Button with Icon -->
<button type="button" class="inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-radius bg-surface-dark border border-surface-dark dark:border-surface px-4 py-2 text-sm font-medium tracking-wide text-on-surface-dark transition hover:opacity-75 text-center 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:opacity-75 disabled:cursor-not-allowed 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>
Create
</button>
<!-- info Button with Icon -->
<button type="button" class="inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-radius bg-info border border-info dark:border-info px-4 py-2 text-sm font-medium tracking-wide text-on-info transition hover:opacity-75 text-center focus-visible:outline 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: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>
Create
</button>
<!-- danger Button with Icon -->
<button type="button" class="inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-radius bg-danger border border-danger dark:border-danger px-4 py-2 text-sm font-medium tracking-wide text-on-danger transition hover:opacity-75 text-center focus-visible:outline 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: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>
Create
</button>
<!-- warning Button with Icon -->
<button type="button" class="inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-radius bg-warning border border-warning dark:border-warning px-4 py-2 text-sm font-medium tracking-wide text-on-warning transition hover:opacity-75 text-center focus-visible:outline 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: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>
Create
</button>
<!-- success Button with Icon -->
<button type="button" class="inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-radius bg-success border border-success dark:border-success px-4 py-2 text-sm font-medium tracking-wide text-on-success transition hover:opacity-75 text-center focus-visible:outline 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: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>
Create
</button>

View File

@@ -0,0 +1,24 @@
<!-- primary Button -->
<button type="button" class="whitespace-nowrap rounded-radius bg-primary border border-primary px-4 py-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:bg-primary-dark dark:border-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark">Primary</button>
<!-- secondary Button -->
<button type="button" class="whitespace-nowrap rounded-radius bg-secondary border border-secondary px-4 py-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:bg-secondary-dark dark:border-secondary-dark dark:text-on-secondary-dark dark:focus-visible:outline-secondary-dark">Secondary</button>
<!-- alternate Button -->
<button type="button" class="whitespace-nowrap rounded-radius bg-surface-alt border border-surface-alt px-4 py-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:bg-surface-dark-alt dark:border-surface-dark-alt dark:text-on-surface-dark-strong dark:focus-visible:outline-surface-dark-alt">Alternate</button>
<!-- inverse Button -->
<button type="button" class="whitespace-nowrap rounded-radius bg-surface-dark border border-surface-dark px-4 py-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:bg-surface dark:border-surface dark:text-on-surface dark:focus-visible:outline-surface">Inverse</button>
<!-- info Button -->
<button type="button" class="whitespace-nowrap rounded-radius bg-info border border-info px-4 py-2 text-sm font-medium tracking-wide text-onInfo 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:bg-info dark:border-info dark:text-onInfo dark:focus-visible:outline-info">Info</button>
<!-- danger Button -->
<button type="button" class="whitespace-nowrap rounded-radius bg-danger border border-danger px-4 py-2 text-sm font-medium tracking-wide text-onDanger 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:bg-danger dark:border-danger dark:text-onDanger dark:focus-visible:outline-danger">Danger</button>
<!-- warning Button -->
<button type="button" class="whitespace-nowrap rounded-radius bg-warning border border-warning px-4 py-2 text-sm font-medium tracking-wide text-onWarning 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:bg-warning dark:border-warning dark:text-onWarning dark:focus-visible:outline-warning">Warning</button>
<!-- success Button -->
<button type="button" class="whitespace-nowrap rounded-radius bg-success border border-success px-4 py-2 text-sm font-medium tracking-wide text-onSuccess 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:bg-success dark:border-success dark:text-onSuccess dark:focus-visible:outline-success">Success</button>

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>

View File

@@ -0,0 +1,24 @@
<!-- primary Ghost Button -->
<button type="button" class="bg-transparent rounded-radius px-4 py-2 text-sm font-medium tracking-wide text-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:text-primary-dark dark:focus-visible:outline-primary-dark">Primary</button>
<!-- secondary Ghost Button -->
<button type="button" class="bg-transparent rounded-radius px-4 py-2 text-sm font-medium tracking-wide text-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:text-secondary-dark dark:focus-visible:outline-secondary-dark">Secondary</button>
<!-- alternate Ghost Button -->
<button type="button" class="bg-transparent rounded-radius px-4 py-2 text-sm font-medium tracking-wide text-outline transition hover:opacity-75 text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-outline active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:text-outline-dark dark:focus-visible:outline-outline-dark">Alternate</button>
<!-- inverse Ghost Button -->
<button type="button" class="bg-transparent rounded-radius px-4 py-2 text-sm font-medium tracking-wide text-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:text-surface dark:focus-visible:outline-surface">Inverse</button>
<!-- info Ghost Button -->
<button type="button" class="bg-transparent rounded-radius px-4 py-2 text-sm font-medium tracking-wide text-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:text-info dark:focus-visible:outline-info">Info</button>
<!-- danger Ghost Button -->
<button type="button" class="bg-transparent rounded-radius px-4 py-2 text-sm font-medium tracking-wide text-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:text-danger dark:focus-visible:outline-danger">Danger</button>
<!-- warning Ghost Button -->
<button type="button" class="bg-transparent rounded-radius px-4 py-2 text-sm font-medium tracking-wide text-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:text-warning dark:focus-visible:outline-warning">Warning</button>
<!-- success Ghost Button -->
<button type="button" class="bg-transparent rounded-radius px-4 py-2 text-sm font-medium tracking-wide text-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:text-success dark:focus-visible:outline-success">Success</button>

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