Compare commits
32 Commits
v0.1.2
...
e51eda9a8c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e51eda9a8c | ||
|
|
12e00a782d | ||
|
|
5278988842 | ||
|
|
e70743996b | ||
|
|
11762728c9 | ||
|
|
ebb208baba | ||
|
|
7cba3d9eba | ||
|
|
35e2b6edc9 | ||
|
|
f3daa27ce7 | ||
|
|
46cc2459bd | ||
|
|
996358be87 | ||
|
|
c6624e1b3d | ||
|
|
b9c1277876 | ||
|
|
42bab82960 | ||
|
|
7da4109584 | ||
|
|
ed607e3d27 | ||
|
|
7af0a48e92 | ||
|
|
1cd2b86b74 | ||
|
|
68381d558a | ||
|
|
36a5e7c5fc | ||
|
|
e8c6035eeb | ||
|
|
9a3c68eae5 | ||
|
|
ee944ed5ce | ||
|
|
0a619517b6 | ||
|
|
1538d870b9 | ||
|
|
ed2eb036ae | ||
|
|
ae99ec079f | ||
|
|
0754e014a3 | ||
|
|
1d747d9960 | ||
|
|
126b1eeb7e | ||
|
|
c401acb1cc | ||
|
|
67fd364761 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,6 +19,8 @@ target/
|
|||||||
*.sqlite-*
|
*.sqlite-*
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
|
.envrc
|
||||||
|
.direnv/
|
||||||
uploads/
|
uploads/
|
||||||
*.report.html
|
*.report.html
|
||||||
favicon_io.zip
|
favicon_io.zip
|
||||||
|
|||||||
1599
Cargo.lock
generated
1599
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
@@ -3,7 +3,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kompress_eshop"
|
name = "kompress_eshop"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
default-run = "kompress-eshop-cli"
|
default-run = "kompress-eshop-cli"
|
||||||
|
|
||||||
@@ -16,14 +16,14 @@ loco-rs = { version = "0.16" }
|
|||||||
loco-rs = { workspace = true }
|
loco-rs = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
tokio = { version = "1.45", default-features = false, features = [
|
tokio = { version = "1.52", default-features = false, features = [
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
] }
|
] }
|
||||||
async-trait = { version = "0.1" }
|
async-trait = { version = "0.1" }
|
||||||
axum = { version = "0.8", features = ["multipart"] }
|
axum = { version = "0.8", features = ["multipart"] }
|
||||||
tracing = { version = "0.1" }
|
tracing = { version = "0.1" }
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
regex = { version = "1.11" }
|
regex = { version = "1.12" }
|
||||||
migration = { path = "migration" }
|
migration = { path = "migration" }
|
||||||
sea-orm = { version = "1.1", features = [
|
sea-orm = { version = "1.1", features = [
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
@@ -35,7 +35,7 @@ chrono = { version = "0.4" }
|
|||||||
time = { version = "0.3" }
|
time = { version = "0.3" }
|
||||||
dotenvy = { version = "0.15" }
|
dotenvy = { version = "0.15" }
|
||||||
validator = { version = "0.20" }
|
validator = { version = "0.20" }
|
||||||
uuid = { version = "1.6", features = ["v4"] }
|
uuid = { version = "1.23", features = ["v4"] }
|
||||||
include_dir = { version = "0.7" }
|
include_dir = { version = "0.7" }
|
||||||
# outbound HTTP for carrier shipment APIs (Packeta / DPD / DHL)
|
# outbound HTTP for carrier shipment APIs (Packeta / DPD / DHL)
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
@@ -45,6 +45,10 @@ unic-langid = { version = "0.9" }
|
|||||||
# /view engine
|
# /view engine
|
||||||
axum-extra = { version = "0.10", features = ["form"] }
|
axum-extra = { version = "0.10", features = ["form"] }
|
||||||
bytes = { version = "1" }
|
bytes = { version = "1" }
|
||||||
|
axum-casbin = "1.3.0"
|
||||||
|
loco-oauth2 = "0.5.0"
|
||||||
|
passwords = "3.1.16"
|
||||||
|
tower-sessions = "0.14"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "kompress-eshop-cli"
|
name = "kompress-eshop-cli"
|
||||||
@@ -53,6 +57,6 @@ required-features = []
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
loco-rs = { workspace = true, features = ["testing"] }
|
loco-rs = { workspace = true, features = ["testing"] }
|
||||||
serial_test = { version = "3.1.1" }
|
serial_test = { version = "3.5.0" }
|
||||||
rstest = { version = "0.25" }
|
rstest = { version = "0.25" }
|
||||||
insta = { version = "1.34", features = ["redactions", "yaml", "filters"] }
|
insta = { version = "1.48", features = ["redactions", "yaml", "filters"] }
|
||||||
|
|||||||
@@ -18,6 +18,12 @@
|
|||||||
/* Scan every template so used utility classes are emitted. */
|
/* Scan every template so used utility classes are emitted. */
|
||||||
@source "../views";
|
@source "../views";
|
||||||
|
|
||||||
|
/* penguinui-components/ is the read-only vendored PenguinUI library
|
||||||
|
* (reference only — never {% include %}'d, never edited). Tailwind v4
|
||||||
|
* auto-detects it from the project root, so exclude it explicitly or
|
||||||
|
* its 177 files bloat the build with classes we never render. */
|
||||||
|
@source not "../../penguinui-components";
|
||||||
|
|
||||||
/* PenguinUI toggles dark styles with a `dark:` variant. This app
|
/* PenguinUI toggles dark styles with a `dark:` variant. This app
|
||||||
* already sets <html data-theme="dark|light"> (see base.html), so
|
* already sets <html data-theme="dark|light"> (see base.html), so
|
||||||
* key the variant off that attribute instead of the OS setting. */
|
* key the variant off that attribute instead of the OS setting. */
|
||||||
|
|||||||
@@ -57,12 +57,31 @@ album-by = by
|
|||||||
album-play-full = Play full album
|
album-play-full = Play full album
|
||||||
album-queue-all = queue all tracks in order
|
album-queue-all = queue all tracks in order
|
||||||
album-no-tracks = no tracks yet
|
album-no-tracks = no tracks yet
|
||||||
login-title = Admin login
|
login-title = Sign in
|
||||||
login-error = Access denied - invalid email or password.
|
login-error = Access denied - invalid email or password.
|
||||||
|
login-error-unverified = Your account isn't verified yet. Check your email and click the verification link.
|
||||||
login-root = root
|
login-root = root
|
||||||
login-auth = Authenticate
|
login-auth = Sign in
|
||||||
login-email = Email
|
login-email = Email
|
||||||
login-password = Password
|
login-password = Password
|
||||||
|
login-no-account = Don't have an account?
|
||||||
|
login-have-account = Already have an account?
|
||||||
|
auth-or = or
|
||||||
|
auth-google = Continue with Google
|
||||||
|
nav-login = Sign in
|
||||||
|
nav-register = Register
|
||||||
|
nav-profile = My profile
|
||||||
|
register-title = Create account
|
||||||
|
register-name = Name
|
||||||
|
register-submit = Create account
|
||||||
|
register-error-exists = An account with this email already exists.
|
||||||
|
register-error-invalid = Please check the details you entered and try again.
|
||||||
|
verify-sent-title = Check your email
|
||||||
|
verify-sent-body = We've sent a verification link to
|
||||||
|
verify-ok-title = Account verified
|
||||||
|
verify-ok-body = Your account is verified. You can now sign in.
|
||||||
|
verify-fail-title = Verification failed
|
||||||
|
verify-fail-body = This link is invalid or has expired.
|
||||||
auth = Auth
|
auth = Auth
|
||||||
admin-session = Session
|
admin-session = Session
|
||||||
readonly = readonly
|
readonly = readonly
|
||||||
@@ -202,8 +221,11 @@ parent-category = Parent category
|
|||||||
no-parent = — None (top level) —
|
no-parent = — None (top level) —
|
||||||
quantity = Quantity
|
quantity = Quantity
|
||||||
add-to-cart = Add to cart
|
add-to-cart = Add to cart
|
||||||
|
cart-added = Added to cart
|
||||||
in-stock = In stock
|
in-stock = In stock
|
||||||
out-of-stock = Out of stock
|
out-of-stock = Out of stock
|
||||||
|
gallery-prev = Previous image
|
||||||
|
gallery-next = Next image
|
||||||
confirm-delete = Delete this for good?
|
confirm-delete = Delete this for good?
|
||||||
shop-title = Shop
|
shop-title = Shop
|
||||||
shop-subtitle = browse our products.
|
shop-subtitle = browse our products.
|
||||||
@@ -235,8 +257,45 @@ country-de = Germany
|
|||||||
country-pl = Poland
|
country-pl = Poland
|
||||||
country-hu = Hungary
|
country-hu = Hungary
|
||||||
checkout-note = Order note
|
checkout-note = Order note
|
||||||
|
checkout-save-profile = Save this address to my profile
|
||||||
|
account-type = Account type
|
||||||
|
account-personal = Individual
|
||||||
|
account-company = Company
|
||||||
|
account-company-details = Company details
|
||||||
|
company-name = Company name
|
||||||
|
company-ico = Company ID (IČO)
|
||||||
|
company-dic = Tax ID (DIČ)
|
||||||
|
company-icdph = VAT ID (IČ DPH)
|
||||||
|
field-optional = optional
|
||||||
checkout-place-order = Place order
|
checkout-place-order = Place order
|
||||||
checkout-summary = Order summary
|
checkout-summary = Order summary
|
||||||
|
profile-title = My profile
|
||||||
|
profile-intro = We'll use these details to prefill checkout.
|
||||||
|
profile-saved = Profile saved.
|
||||||
|
profile-save = Save profile
|
||||||
|
profile-company-required = For a company account, please fill in company name, IČO and DIČ.
|
||||||
|
profile-first-name = First name
|
||||||
|
profile-last-name = Surname
|
||||||
|
profile-edit = Edit profile
|
||||||
|
profile-cancel = Cancel
|
||||||
|
profile-not-set = Not set
|
||||||
|
account-type-locked = Account type can't be changed after registration.
|
||||||
|
checkout-create-account = Create an account from this order
|
||||||
|
checkout-create-account-hint = We'll email you a link to set your password. This order will be linked to your account.
|
||||||
|
order-account-created = We created an account for you. Check your email to set your password.
|
||||||
|
set-password-title = Set your password
|
||||||
|
set-password-intro = Choose a password to finish setting up your account.
|
||||||
|
set-password-new = New password
|
||||||
|
set-password-confirm = Confirm password
|
||||||
|
set-password-submit = Set password
|
||||||
|
set-password-invalid = This link is invalid or has expired.
|
||||||
|
set-password-weak = Password must be at least 8 characters.
|
||||||
|
set-password-mismatch = Passwords don't match.
|
||||||
|
resend-verification-title = Resend verification email
|
||||||
|
resend-verification-intro = Enter your email and we'll send a fresh verification link.
|
||||||
|
resend-verification-submit = Resend
|
||||||
|
resend-verification-done = If that email belongs to an unverified account, we've sent a new verification link. Check your inbox (and spam). You can request another in a minute.
|
||||||
|
login-resend = Didn't get the verification email? Resend it
|
||||||
order-confirmed-title = Thank you for your order!
|
order-confirmed-title = Thank you for your order!
|
||||||
order-confirmed-sub = We have received your order.
|
order-confirmed-sub = We have received your order.
|
||||||
order-number = Order number
|
order-number = Order number
|
||||||
|
|||||||
@@ -57,12 +57,31 @@ album-by = od
|
|||||||
album-play-full = Prehrať celý album
|
album-play-full = Prehrať celý album
|
||||||
album-queue-all = zoradiť všetky skladby v poradí
|
album-queue-all = zoradiť všetky skladby v poradí
|
||||||
album-no-tracks = zatiaľ žiadne skladby
|
album-no-tracks = zatiaľ žiadne skladby
|
||||||
login-title = Prihlásenie admina
|
login-title = Prihlásenie
|
||||||
login-error = Prístup odmietnutý - nesprávny e-mail alebo heslo.
|
login-error = Prístup odmietnutý - nesprávny e-mail alebo heslo.
|
||||||
|
login-error-unverified = Účet ešte nie je overený. Skontrolujte si e-mail a kliknite na overovací odkaz.
|
||||||
login-root = root
|
login-root = root
|
||||||
login-auth = Prihlásiť sa
|
login-auth = Prihlásiť sa
|
||||||
login-email = E-mail
|
login-email = E-mail
|
||||||
login-password = Heslo
|
login-password = Heslo
|
||||||
|
login-no-account = Nemáte účet?
|
||||||
|
login-have-account = Už máte účet?
|
||||||
|
auth-or = alebo
|
||||||
|
auth-google = Pokračovať cez Google
|
||||||
|
nav-login = Prihlásiť sa
|
||||||
|
nav-register = Registrácia
|
||||||
|
nav-profile = Môj profil
|
||||||
|
register-title = Vytvoriť účet
|
||||||
|
register-name = Meno
|
||||||
|
register-submit = Zaregistrovať sa
|
||||||
|
register-error-exists = Účet s týmto e-mailom už existuje.
|
||||||
|
register-error-invalid = Skontrolujte zadané údaje a skúste to znova.
|
||||||
|
verify-sent-title = Skontrolujte si e-mail
|
||||||
|
verify-sent-body = Poslali sme overovací odkaz na adresu
|
||||||
|
verify-ok-title = Účet overený
|
||||||
|
verify-ok-body = Váš účet je overený. Teraz sa môžete prihlásiť.
|
||||||
|
verify-fail-title = Overenie zlyhalo
|
||||||
|
verify-fail-body = Tento odkaz je neplatný alebo mu vypršala platnosť.
|
||||||
auth = Overenie
|
auth = Overenie
|
||||||
admin-session = Relácia
|
admin-session = Relácia
|
||||||
readonly = iba na čítanie
|
readonly = iba na čítanie
|
||||||
@@ -202,8 +221,11 @@ parent-category = Nadradená kategória
|
|||||||
no-parent = — Žiadna (najvyššia úroveň) —
|
no-parent = — Žiadna (najvyššia úroveň) —
|
||||||
quantity = Množstvo
|
quantity = Množstvo
|
||||||
add-to-cart = Pridať do košíka
|
add-to-cart = Pridať do košíka
|
||||||
|
cart-added = Pridané do košíka
|
||||||
in-stock = Na sklade
|
in-stock = Na sklade
|
||||||
out-of-stock = Vypredané
|
out-of-stock = Vypredané
|
||||||
|
gallery-prev = Predchádzajúci obrázok
|
||||||
|
gallery-next = Ďalší obrázok
|
||||||
confirm-delete = Naozaj zmazať?
|
confirm-delete = Naozaj zmazať?
|
||||||
shop-title = Obchod
|
shop-title = Obchod
|
||||||
shop-subtitle = prezrite si našu ponuku produktov.
|
shop-subtitle = prezrite si našu ponuku produktov.
|
||||||
@@ -235,8 +257,45 @@ country-de = Nemecko
|
|||||||
country-pl = Poľsko
|
country-pl = Poľsko
|
||||||
country-hu = Maďarsko
|
country-hu = Maďarsko
|
||||||
checkout-note = Poznámka k objednávke
|
checkout-note = Poznámka k objednávke
|
||||||
|
checkout-save-profile = Uložiť túto adresu do môjho profilu
|
||||||
|
account-type = Typ účtu
|
||||||
|
account-personal = Súkromná osoba
|
||||||
|
account-company = Firma
|
||||||
|
account-company-details = Firemné údaje
|
||||||
|
company-name = Názov firmy
|
||||||
|
company-ico = IČO
|
||||||
|
company-dic = DIČ
|
||||||
|
company-icdph = IČ DPH
|
||||||
|
field-optional = nepovinné
|
||||||
checkout-place-order = Odoslať objednávku
|
checkout-place-order = Odoslať objednávku
|
||||||
checkout-summary = Súhrn objednávky
|
checkout-summary = Súhrn objednávky
|
||||||
|
profile-title = Môj profil
|
||||||
|
profile-intro = Tieto údaje použijeme na predvyplnenie pokladne.
|
||||||
|
profile-saved = Profil bol uložený.
|
||||||
|
profile-save = Uložiť profil
|
||||||
|
profile-company-required = Pri firemnom účte vyplňte názov firmy, IČO a DIČ.
|
||||||
|
profile-first-name = Meno
|
||||||
|
profile-last-name = Priezvisko
|
||||||
|
profile-edit = Upraviť profil
|
||||||
|
profile-cancel = Zrušiť
|
||||||
|
profile-not-set = Neuvedené
|
||||||
|
account-type-locked = Typ účtu sa po registrácii nedá zmeniť.
|
||||||
|
checkout-create-account = Vytvoriť účet z tejto objednávky
|
||||||
|
checkout-create-account-hint = Pošleme vám e-mail na nastavenie hesla. Objednávka sa priradí k vášmu účtu.
|
||||||
|
order-account-created = Vytvorili sme vám účet. Skontrolujte si e-mail a nastavte si heslo.
|
||||||
|
set-password-title = Nastavte si heslo
|
||||||
|
set-password-intro = Zvoľte si heslo a dokončite vytvorenie účtu.
|
||||||
|
set-password-new = Nové heslo
|
||||||
|
set-password-confirm = Potvrďte heslo
|
||||||
|
set-password-submit = Nastaviť heslo
|
||||||
|
set-password-invalid = Odkaz je neplatný alebo vypršal.
|
||||||
|
set-password-weak = Heslo musí mať aspoň 8 znakov.
|
||||||
|
set-password-mismatch = Heslá sa nezhodujú.
|
||||||
|
resend-verification-title = Znova odoslať overovací e-mail
|
||||||
|
resend-verification-intro = Zadajte svoj e-mail a pošleme vám nový overovací odkaz.
|
||||||
|
resend-verification-submit = Odoslať znova
|
||||||
|
resend-verification-done = Ak k tomuto e-mailu patrí neoverený účet, poslali sme naň nový overovací odkaz. Skontrolujte si schránku aj priečinok so spamom. Ďalšiu žiadosť môžete odoslať o minútu.
|
||||||
|
login-resend = Nedostali ste overovací e-mail? Poslať znova
|
||||||
order-confirmed-title = Ďakujeme za objednávku!
|
order-confirmed-title = Ďakujeme za objednávku!
|
||||||
order-confirmed-sub = Vašu objednávku sme prijali.
|
order-confirmed-sub = Vašu objednávku sme prijali.
|
||||||
order-number = Číslo objednávky
|
order-number = Číslo objednávky
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
228
assets/views/account/profile.html
Normal file
228
assets/views/account/profile.html
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
|
{% block title %}{{ t(key="profile-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||||
|
|
||||||
|
{% macro field(label, value) %}
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ label }}</label>
|
||||||
|
{% if value %}
|
||||||
|
<p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">{{ value }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-sm italic text-on-surface/50 dark:text-on-surface-dark/50">{{ t(key="profile-not-set", lang=lang | default(value='sk')) }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro field %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mx-auto max-w-2xl" x-data="{ editing: {% if error %}true{% else %}false{% endif %} }">
|
||||||
|
<h1 class="text-3xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="profile-title", lang=lang | default(value='sk')) }}</h1>
|
||||||
|
<p class="mt-2 text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="profile-intro", lang=lang | default(value='sk')) }}</p>
|
||||||
|
|
||||||
|
{% if saved %}
|
||||||
|
<div class="mt-4 rounded-radius border border-success bg-success/10 px-4 py-3 text-sm text-success" role="status">
|
||||||
|
{{ t(key="profile-saved", lang=lang | default(value='sk')) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if error %}
|
||||||
|
{{ ui::alert_danger(message=t(key="profile-company-required", lang=lang | default(value='sk')), extra="mt-4") }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- read-only view (default) -->
|
||||||
|
<div x-show="!editing" class="mt-6 space-y-6">
|
||||||
|
<fieldset class="space-y-2 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-type", lang=lang | default(value='sk')) }}</legend>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{% if account_type == "company" %}
|
||||||
|
{{ ui::badge(label=t(key="account-company", lang=lang | default(value='sk')), variant="primary") }}
|
||||||
|
{% else %}
|
||||||
|
{{ ui::badge(label=t(key="account-personal", lang=lang | default(value='sk')), variant="neutral") }}
|
||||||
|
{% endif %}
|
||||||
|
<span class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-type-locked", lang=lang | default(value='sk')) }}</span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{% if account_type == "company" %}
|
||||||
|
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company-details", lang=lang | default(value='sk')) }}</legend>
|
||||||
|
{{ self::field(label=t(key="company-name", lang=lang | default(value='sk')), value=company_name) }}
|
||||||
|
<div class="grid gap-4 sm:grid-cols-3">
|
||||||
|
{{ self::field(label=t(key="company-ico", lang=lang | default(value='sk')), value=company_id) }}
|
||||||
|
{{ self::field(label=t(key="company-dic", lang=lang | default(value='sk')), value=tax_id) }}
|
||||||
|
{{ self::field(label=t(key="company-icdph", lang=lang | default(value='sk')), value=vat_id) }}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend>
|
||||||
|
{{ self::field(label=t(key="checkout-name", lang=lang | default(value='sk')), value=name) }}
|
||||||
|
{{ self::field(label=t(key="checkout-email", lang=lang | default(value='sk')), value=email) }}
|
||||||
|
{% if phone %}
|
||||||
|
{% set phone_full = phone_prefix | default(value='') %}
|
||||||
|
{% set phone_full = phone_full ~ ' ' ~ phone %}
|
||||||
|
{{ self::field(label=t(key="checkout-phone", lang=lang | default(value='sk')), value=phone_full) }}
|
||||||
|
{% else %}
|
||||||
|
{{ self::field(label=t(key="checkout-phone", lang=lang | default(value='sk')), value='') }}
|
||||||
|
{% endif %}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</legend>
|
||||||
|
{{ self::field(label=t(key="checkout-address", lang=lang | default(value='sk')), value=address) }}
|
||||||
|
<div class="grid gap-4 sm:grid-cols-3">
|
||||||
|
{{ self::field(label=t(key="checkout-city", lang=lang | default(value='sk')), value=city) }}
|
||||||
|
{{ self::field(label=t(key="checkout-zip", lang=lang | default(value='sk')), value=zip) }}
|
||||||
|
{{ self::field(label=t(key="checkout-country", lang=lang | default(value='sk')), value=country) }}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{{ ui::button(label=t(key="profile-edit", lang=lang | default(value='sk')), type="button", size="px-6 py-2.5 text-sm", attrs='@click="editing = true"') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- edit form -->
|
||||||
|
<form x-show="editing" x-cloak method="post" action="/account/profile" hx-boost="false" class="mt-6 space-y-6">
|
||||||
|
<!-- account type is fixed at registration and shown read-only -->
|
||||||
|
<fieldset class="space-y-2 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-type", lang=lang | default(value='sk')) }}</legend>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{% if account_type == "company" %}
|
||||||
|
{{ ui::badge(label=t(key="account-company", lang=lang | default(value='sk')), variant="primary") }}
|
||||||
|
{% else %}
|
||||||
|
{{ ui::badge(label=t(key="account-personal", lang=lang | default(value='sk')), variant="neutral") }}
|
||||||
|
{% endif %}
|
||||||
|
<span class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-type-locked", lang=lang | default(value='sk')) }}</span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{% if account_type == "company" %}
|
||||||
|
<!-- company billing details (company accounts only) -->
|
||||||
|
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company-details", lang=lang | default(value='sk')) }}</legend>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="company_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-name", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
|
{{ ui::input(name="company_name", id="company_name", value=company_name | default(value=''), autocomplete="organization") }}
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-3">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="company_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-ico", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
|
{{ ui::input(name="company_id", id="company_id", value=company_id | default(value='')) }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="tax_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-dic", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
|
{{ ui::input(name="tax_id", id="tax_id", value=tax_id | default(value='')) }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="vat_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-icdph", lang=lang | default(value='sk')) }} <span class="text-on-surface/50 dark:text-on-surface-dark/50">({{ t(key="field-optional", lang=lang | default(value='sk')) }})</span></label>
|
||||||
|
{{ ui::input(name="vat_id", id="vat_id", value=vat_id | default(value='')) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- contact (name/email are managed by the login) -->
|
||||||
|
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="first_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="profile-first-name", lang=lang | default(value='sk')) }}</label>
|
||||||
|
{{ ui::input(name="first_name", id="first_name", value=first_name | default(value=''), autocomplete="given-name") }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="last_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="profile-last-name", lang=lang | default(value='sk')) }}</label>
|
||||||
|
{{ ui::input(name="last_name", id="last_name", value=last_name | default(value=''), autocomplete="family-name") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-email", lang=lang | default(value='sk')) }}</label>
|
||||||
|
<p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">{{ email }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="phone" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-phone", lang=lang | default(value='sk')) }}</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<!-- editable combobox: type freely or pick from the dropdown -->
|
||||||
|
<div class="relative w-28 shrink-0" @click.outside="prefixOpen = false"
|
||||||
|
x-data="{ prefixOpen: false, prefix: '{{ phone_prefix | default(value='+421') }}', opts: [
|
||||||
|
{ v: '+421', l: '🇸🇰 +421' }, { v: '+420', l: '🇨🇿 +420' },
|
||||||
|
{ v: '+43', l: '🇦🇹 +43' }, { v: '+49', l: '🇩🇪 +49' },
|
||||||
|
{ v: '+48', l: '🇵🇱 +48' }, { v: '+36', l: '🇭🇺 +36' },
|
||||||
|
{ v: '+44', l: '🇬🇧 +44' }, { v: '+39', l: '🇮🇹 +39' }, { v: '+33', l: '🇫🇷 +33' }
|
||||||
|
], get filtered() { return this.opts.filter(o => !this.prefix || o.v.includes(this.prefix)) } }">
|
||||||
|
<input name="phone_prefix" type="text" x-model="prefix" @focus="prefixOpen = true" @input="prefixOpen = true"
|
||||||
|
aria-label="{{ t(key='checkout-phone', lang=lang | default(value='sk')) }}" autocomplete="tel-country-code" inputmode="tel"
|
||||||
|
class="w-full rounded-radius border border-outline bg-surface py-2 pl-3 pr-7 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||||
|
<button type="button" tabindex="-1" @click="prefixOpen = !prefixOpen"
|
||||||
|
class="absolute inset-y-0 right-0 flex w-7 items-center justify-center text-on-surface/60 dark:text-on-surface-dark/60">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
|
||||||
|
class="size-4 transition-transform" :class="prefixOpen && 'rotate-180'">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul x-show="prefixOpen" x-cloak x-transition
|
||||||
|
class="absolute z-20 mt-1 max-h-56 w-full overflow-auto rounded-radius border border-outline bg-surface p-1 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<template x-for="o in filtered" :key="o.v">
|
||||||
|
<li><button type="button" @click="prefix = o.v; prefixOpen = false" x-text="o.l"
|
||||||
|
class="block w-full rounded-radius px-3 py-1.5 text-left text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark"></button></li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{ ui::input(name="phone", id="phone", type="tel", value=phone | default(value=''), autocomplete="tel", placeholder="900 000 000", attrs='inputmode="tel"') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- default shipping address -->
|
||||||
|
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</legend>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="address" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-address", lang=lang | default(value='sk')) }}</label>
|
||||||
|
{{ ui::input(name="address", id="address", value=address | default(value=''), autocomplete="street-address") }}
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-3">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="city" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-city", lang=lang | default(value='sk')) }}</label>
|
||||||
|
{{ ui::input(name="city", id="city", value=city | default(value=''), autocomplete="address-level2") }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="zip" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-zip", lang=lang | default(value='sk')) }}</label>
|
||||||
|
{{ ui::input(name="zip", id="zip", value=zip | default(value=''), autocomplete="postal-code") }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="country" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-country", lang=lang | default(value='sk')) }}</label>
|
||||||
|
<div class="relative" @click.outside="countryOpen = false"
|
||||||
|
x-data="{ countryOpen: false, country: '{{ country | default(value='') }}', opts: [
|
||||||
|
{ v: '{{ t(key='country-sk', lang=lang | default(value='sk')) }}', l: '🇸🇰 {{ t(key='country-sk', lang=lang | default(value='sk')) }}' },
|
||||||
|
{ v: '{{ t(key='country-cz', lang=lang | default(value='sk')) }}', l: '🇨🇿 {{ t(key='country-cz', lang=lang | default(value='sk')) }}' },
|
||||||
|
{ v: '{{ t(key='country-at', lang=lang | default(value='sk')) }}', l: '🇦🇹 {{ t(key='country-at', lang=lang | default(value='sk')) }}' },
|
||||||
|
{ v: '{{ t(key='country-de', lang=lang | default(value='sk')) }}', l: '🇩🇪 {{ t(key='country-de', lang=lang | default(value='sk')) }}' },
|
||||||
|
{ v: '{{ t(key='country-pl', lang=lang | default(value='sk')) }}', l: '🇵🇱 {{ t(key='country-pl', lang=lang | default(value='sk')) }}' },
|
||||||
|
{ v: '{{ t(key='country-hu', lang=lang | default(value='sk')) }}', l: '🇭🇺 {{ t(key='country-hu', lang=lang | default(value='sk')) }}' }
|
||||||
|
], get filtered() { return this.opts.filter(o => !this.country || o.v.toLowerCase().includes(this.country.toLowerCase())) } }">
|
||||||
|
<input id="country" name="country" type="text" x-model="country" @focus="countryOpen = true" @input="countryOpen = true"
|
||||||
|
class="w-full rounded-radius border border-outline bg-surface py-2 pl-3 pr-8 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
||||||
|
<button type="button" tabindex="-1" @click="countryOpen = !countryOpen"
|
||||||
|
class="absolute inset-y-0 right-0 flex w-8 items-center justify-center text-on-surface/60 dark:text-on-surface-dark/60">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
|
||||||
|
class="size-4 transition-transform" :class="countryOpen && 'rotate-180'">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul x-show="countryOpen" x-cloak x-transition
|
||||||
|
class="absolute z-20 mt-1 max-h-56 w-full overflow-auto rounded-radius border border-outline bg-surface p-1 shadow-lg dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<template x-for="o in filtered" :key="o.v">
|
||||||
|
<li><button type="button" @click="country = o.v; countryOpen = false" x-text="o.l"
|
||||||
|
class="block w-full rounded-radius px-3 py-1.5 text-left text-sm text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark"></button></li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{{ ui::button(label=t(key="profile-save", lang=lang | default(value='sk')), type="submit", size="px-6 py-2.5 text-sm") }}
|
||||||
|
{{ ui::button(label=t(key="profile-cancel", lang=lang | default(value='sk')), type="button", variant="outline-secondary", size="px-6 py-2.5 text-sm", attrs='@click="editing = false"') }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="{{ lang | default(value='sk') }}" data-theme="dark">
|
<html lang="{{ lang | default(value='sk') }}" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
@@ -57,6 +58,11 @@
|
|||||||
x-bind:class="showSidebar ? 'translate-x-0' : '-translate-x-60'"
|
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">
|
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"
|
<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">
|
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')) }}
|
{{ 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">
|
<div class="flex flex-1 flex-col gap-1 overflow-y-auto p-4">
|
||||||
<a href="/admin/dashboard" data-nav="/admin/dashboard"
|
<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')) }}
|
{{ t(key="admin-dashboard", lang=lang | default(value='sk')) }}
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/catalog/products" data-nav="/admin/catalog/products"
|
<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')) }}
|
{{ t(key="admin-products", lang=lang | default(value='sk')) }}
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/catalog/categories" data-nav="/admin/catalog/categories"
|
<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')) }}
|
{{ t(key="admin-categories", lang=lang | default(value='sk')) }}
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/orders" data-nav="/admin/orders"
|
<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')) }}
|
{{ t(key="admin-orders", lang=lang | default(value='sk')) }}
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/shipping" data-nav="/admin/shipping"
|
<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')) }}
|
{{ t(key="admin-shipping", lang=lang | default(value='sk')) }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-outline p-4 dark:border-outline-dark">
|
<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')) }}
|
{{ t(key="admin-exit", lang=lang | default(value='sk')) }}
|
||||||
</a>
|
</a>
|
||||||
<form method="post" action="/admin/logout">
|
<form method="post" action="/logout">
|
||||||
<button type="submit" class="flex w-full items-center gap-3 rounded-radius px-3 py-2 text-left text-sm font-medium text-danger transition hover:bg-surface dark:hover:bg-surface-dark">
|
<button type="submit" class="flex w-full items-center gap-2 rounded-radius px-2 py-1.5 text-left text-sm font-medium text-danger underline-offset-2 transition hover:bg-danger/5 focus:outline-hidden focus-visible:underline">
|
||||||
{{ t(key="logout", lang=lang | default(value='sk')) }}
|
{{ t(key="logout", lang=lang | default(value='sk')) }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -100,13 +106,11 @@
|
|||||||
<!-- content column -->
|
<!-- content column -->
|
||||||
<div class="flex min-h-screen flex-col md:ml-60">
|
<div class="flex min-h-screen flex-col md:ml-60">
|
||||||
<header class="sticky top-0 z-20 flex h-16 items-center gap-4 border-b border-outline bg-surface/95 px-4 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95">
|
<header class="sticky top-0 z-20 flex h-16 items-center gap-4 border-b border-outline bg-surface/95 px-4 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95">
|
||||||
<button type="button" @click="showSidebar = !showSidebar" :aria-expanded="showSidebar"
|
<!-- Penguin animated hamburger (bars ↔ X) in our ghost-square shell -->
|
||||||
aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
|
<button type="button" @click="showSidebar = !showSidebar" :aria-expanded="showSidebar" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
|
||||||
class="inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt md:hidden dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">
|
class="inline-flex size-9 shrink-0 items-center justify-center rounded-radius bg-transparent text-secondary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 md:hidden dark:text-secondary-dark dark:focus-visible:outline-secondary-dark">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
{{ ui::icon(name="hamburger", size="size-6", attrs='x-show="!showSidebar"') }}
|
||||||
stroke="currentColor" class="size-6">
|
{{ ui::icon(name="close", size="size-6", attrs='x-cloak x-show="showSidebar"') }}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span class="text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">
|
<span class="text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
@@ -115,54 +119,7 @@
|
|||||||
|
|
||||||
<!-- settings (language + theme) dropdown -->
|
<!-- settings (language + theme) dropdown -->
|
||||||
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative ml-auto">
|
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative ml-auto">
|
||||||
<button type="button" @click="open = !open" :aria-expanded="open"
|
{% include "partials/settings_dropdown.html" %}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ t(key="admin-categories", lang=lang | default(value='sk')) }}{% endblock title %}
|
{% 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 %}
|
{% 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>
|
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-categories", lang=lang | default(value='sk')) }}</h1>
|
||||||
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-categories-desc", lang=lang | default(value='sk')) }}</p>
|
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-categories-desc", lang=lang | default(value='sk')) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="/admin/catalog/categories/new"
|
{{ ui::button(label=t(key="new-category", lang=lang | default(value='sk')), 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
|
<div class="mt-6 {{ ui::table_wrap_cls() }}">
|
||||||
{% if categories | length > 0 %}
|
{% if categories | length > 0 %}
|
||||||
<table class="w-full text-left text-sm">
|
<table class="{{ ui::table_cls() }}">
|
||||||
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
|
<thead class="{{ ui::thead_cls() }}">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="name", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="name", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="admin-products", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="admin-products", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="status", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="status", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 text-right font-semibold">{{ t(key="actions", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="actions", lang=lang | default(value='sk')), align="text-right") }}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-outline dark:divide-outline-dark">
|
<tbody class="{{ ui::tbody_cls() }}">
|
||||||
{% for row in categories %}
|
{% for row in categories %}
|
||||||
<tr class="hover:bg-surface-alt dark:hover:bg-surface-dark-alt">
|
<tr class="{{ ui::row_cls() }}">
|
||||||
<td class="px-4 py-3 font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
|
<td class="px-4 py-3 font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
<span style="padding-left: {{ row.depth * 20 }}px" class="inline-flex items-center gap-1.5">
|
<span style="padding-left: {{ row.depth * 20 }}px" class="inline-flex items-center gap-1.5">
|
||||||
{% if row.depth > 0 %}<span class="text-on-surface/40 dark:text-on-surface-dark/40">↳</span>{% endif %}
|
{% if row.depth > 0 %}<span class="text-on-surface/40 dark:text-on-surface-dark/40">↳</span>{% endif %}
|
||||||
@@ -38,18 +36,17 @@
|
|||||||
<td class="px-4 py-3 tabular-nums">{{ row.product_count }}</td>
|
<td class="px-4 py-3 tabular-nums">{{ row.product_count }}</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
{% if row.category.published %}
|
{% 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="flex flex-wrap justify-end gap-2">
|
<div class="flex flex-wrap justify-end gap-2">
|
||||||
<a href="/admin/catalog/categories/{{ row.category.id }}/edit"
|
{{ ui::button(variant="outline-secondary", label=t(key="edit", lang=lang | default(value='sk')), href="/admin/catalog/categories/" ~ row.category.id ~ "/edit", size="px-3 py-1.5 text-xs") }}
|
||||||
class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="edit", lang=lang | default(value='sk')) }}</a>
|
|
||||||
<form method="post" action="/admin/catalog/categories/{{ row.category.id }}/delete"
|
<form method="post" action="/admin/catalog/categories/{{ row.category.id }}/delete"
|
||||||
onsubmit="return confirm('{{ t(key="confirm-delete", lang=lang | default(value='sk')) }}')">
|
onsubmit="return confirm('{{ t(key="confirm-delete", lang=lang | default(value='sk')) }}')">
|
||||||
<button type="submit" class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-danger transition hover:bg-danger/10 dark:border-outline-dark">{{ t(key="delete", lang=lang | default(value='sk')) }}</button>
|
{{ ui::button(variant="outline-danger", label=t(key="delete", lang=lang | default(value='sk')), type="submit", size="px-3 py-1.5 text-xs") }}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -60,10 +57,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="flex flex-col items-center gap-3 px-4 py-16 text-center">
|
<div class="flex flex-col items-center gap-3 px-4 py-16 text-center">
|
||||||
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-no-categories", lang=lang | default(value='sk')) }}</p>
|
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-no-categories", lang=lang | default(value='sk')) }}</p>
|
||||||
<a href="/admin/catalog/categories/new"
|
{{ ui::button(label=t(key="new-category", lang=lang | default(value='sk')), 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>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% 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 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 %}
|
{% 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">
|
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
{% if category %}{{ t(key="edit-category", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-category", lang=lang | default(value='sk')) }}{% endif %}
|
{% if category %}{{ t(key="edit-category", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-category", lang=lang | default(value='sk')) }}{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
<a href="/admin/catalog/categories"
|
{{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/categories", size="px-3 py-2 text-sm") }}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data"
|
<form method="post" enctype="multipart/form-data"
|
||||||
action="{% if category %}/admin/catalog/categories/{{ category.id }}{% else %}/admin/catalog/categories{% endif %}"
|
action="{% if category %}/admin/catalog/categories/{{ category.id }}{% else %}/admin/catalog/categories{% endif %}"
|
||||||
class="mt-6 space-y-5 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
class="mt-6 space-y-5 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
|
||||||
|
{% if category %}
|
||||||
|
{% set v_name = category.name %}{% set v_slug = category.slug %}{% set v_pos = category.position %}{% set v_desc = category.description | default(value="") %}{% set v_pub = category.published %}
|
||||||
|
{% else %}
|
||||||
|
{% set v_name = "" %}{% set v_slug = "" %}{% set v_pos = 0 %}{% set v_desc = "" %}{% set v_pub = false %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="name", lang=lang | default(value='sk')) }}</label>
|
<label for="name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="name", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="name" name="name" type="text" required value="{% if category %}{{ category.name }}{% endif %}"
|
{{ ui::input(name="name", id="name", required=true, value=v_name) }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-5 sm:grid-cols-2">
|
<div class="grid gap-5 sm:grid-cols-2">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="slug" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="slug", lang=lang | default(value='sk')) }}</label>
|
<label for="slug" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="slug", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="slug" name="slug" type="text" value="{% if category %}{{ category.slug }}{% endif %}"
|
{{ ui::input(name="slug", id="slug", value=v_slug, placeholder=t(key='slug-auto', lang=lang | default(value='sk'))) }}
|
||||||
placeholder="{{ t(key='slug-auto', lang=lang | default(value='sk')) }}"
|
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="position" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="position", lang=lang | default(value='sk')) }}</label>
|
<label for="position" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="position", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="position" name="position" type="number" value="{% if category %}{{ category.position }}{% else %}0{% endif %}"
|
{{ ui::input(name="position", id="position", type="number", value=v_pos) }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="parent_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="parent-category", lang=lang | default(value='sk')) }}</label>
|
<label for="parent_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="parent-category", lang=lang | default(value='sk')) }}</label>
|
||||||
<select id="parent_id" name="parent_id"
|
<div class="relative">
|
||||||
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">
|
<select id="parent_id" name="parent_id"
|
||||||
<option value="">{{ t(key="no-parent", lang=lang | default(value='sk')) }}</option>
|
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 parent in parents %}
|
<option value="">{{ t(key="no-parent", lang=lang | default(value='sk')) }}</option>
|
||||||
<option value="{{ parent.id }}" {% if category and category.parent_id == parent.id %}selected{% endif %}>
|
{% for parent in parents %}
|
||||||
{% if parent.depth > 0 %}{% for _ in range(end=parent.depth) %}— {% endfor %}{% endif %}{{ parent.name }}
|
<option value="{{ parent.id }}" {% if category and category.parent_id == parent.id %}selected{% endif %}>
|
||||||
</option>
|
{% if parent.depth > 0 %}{% for _ in range(end=parent.depth) %}— {% endfor %}{% endif %}{{ parent.name }}
|
||||||
{% endfor %}
|
</option>
|
||||||
</select>
|
{% 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>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="description" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="description", lang=lang | default(value='sk')) }}</label>
|
<label for="description" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="description", lang=lang | default(value='sk')) }}</label>
|
||||||
<textarea id="description" name="description" rows="4"
|
{{ ui::textarea(name="description", id="description", rows="4", value=v_desc) }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">{% if category and category.description %}{{ category.description }}{% endif %}</textarea>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
@@ -60,23 +64,14 @@
|
|||||||
{% if category and category.image_id %}
|
{% if category and category.image_id %}
|
||||||
<img src="/images/{{ category.image_id }}" alt="" class="size-24 rounded-radius object-cover">
|
<img src="/images/{{ category.image_id }}" alt="" class="size-24 rounded-radius object-cover">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="image" name="image" type="file" accept="image/*"
|
{{ ui::file_input(name="image", id="image", accept="image/*") }}
|
||||||
class="block w-full text-sm text-on-surface file:mr-3 file:rounded-radius file:border-0 file:bg-primary file:px-3 file:py-2 file:text-sm file:font-medium file:text-on-primary dark:text-on-surface-dark dark:file:bg-primary-dark dark:file:text-on-primary-dark">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="flex items-center gap-2">
|
{{ ui::checkbox(name="published", id="published", label=t(key="published", lang=lang | default(value='sk')), checked=v_pub) }}
|
||||||
<input type="checkbox" name="published" value="on" {% if category and category.published %}checked{% endif %}
|
|
||||||
class="size-4 rounded border-outline text-primary focus:ring-primary dark:border-outline-dark">
|
|
||||||
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="published", lang=lang | default(value='sk')) }}</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex gap-3 pt-2">
|
<div class="flex gap-3 pt-2">
|
||||||
<button type="submit"
|
{{ ui::button(label=t(key="save", lang=lang | default(value='sk')), 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">
|
{{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/categories") }}
|
||||||
{{ 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>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% 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 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 %}
|
{% 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">
|
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
{% if product %}{{ t(key="edit-product", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-product", lang=lang | default(value='sk')) }}{% endif %}
|
{% if product %}{{ t(key="edit-product", lang=lang | default(value='sk')) }}{% else %}{{ t(key="new-product", lang=lang | default(value='sk')) }}{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
<a href="/admin/catalog/products"
|
{{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/products", size="px-3 py-2 text-sm") }}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data"
|
<form method="post" enctype="multipart/form-data"
|
||||||
action="{% if product %}/admin/catalog/products/{{ product.id }}{% else %}/admin/catalog/products{% endif %}"
|
action="{% if product %}/admin/catalog/products/{{ product.id }}{% else %}/admin/catalog/products{% endif %}"
|
||||||
class="mt-6 space-y-5 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
class="mt-6 space-y-5 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
|
||||||
|
{% if product %}
|
||||||
|
{% set v_name = product.name %}{% set v_price = product.price %}{% set v_currency = product.currency %}{% set v_stock = product.stock %}{% set v_sku = product.sku | default(value="") %}{% set v_slug = product.slug %}{% set v_desc = product.description | default(value="") %}{% set v_pub = product.published %}
|
||||||
|
{% else %}
|
||||||
|
{% set v_name = "" %}{% set v_price = "" %}{% set v_currency = "EUR" %}{% set v_stock = 0 %}{% set v_sku = "" %}{% set v_slug = "" %}{% set v_desc = "" %}{% set v_pub = false %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="name", lang=lang | default(value='sk')) }}</label>
|
<label for="name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="name", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="name" name="name" type="text" required value="{% if product %}{{ product.name }}{% endif %}"
|
{{ ui::input(name="name", id="name", required=true, value=v_name) }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-5 sm:grid-cols-2">
|
<div class="grid gap-5 sm:grid-cols-2">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="price" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="price", lang=lang | default(value='sk')) }}</label>
|
<label for="price" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="price", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="price" name="price" type="text" inputmode="decimal" required value="{% if product %}{{ product.price }}{% endif %}"
|
{{ ui::input(name="price", id="price", required=true, value=v_price, placeholder="0.00", attrs='inputmode="decimal"') }}
|
||||||
placeholder="0.00"
|
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="currency" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="currency", lang=lang | default(value='sk')) }}</label>
|
<label for="currency" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="currency", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="currency" name="currency" type="text" maxlength="3" value="{% if product %}{{ product.currency }}{% else %}EUR{% endif %}"
|
{{ ui::input(name="currency", id="currency", value=v_currency, attrs='maxlength="3"', extra="uppercase") }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm uppercase text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-5 sm:grid-cols-2">
|
<div class="grid gap-5 sm:grid-cols-2">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="stock" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="stock", lang=lang | default(value='sk')) }}</label>
|
<label for="stock" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="stock", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="stock" name="stock" type="number" min="0" value="{% if product %}{{ product.stock }}{% else %}0{% endif %}"
|
{{ ui::input(name="stock", id="stock", type="number", value=v_stock, attrs='min="0"') }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="sku" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="sku", lang=lang | default(value='sk')) }}</label>
|
<label for="sku" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="sku", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="sku" name="sku" type="text" value="{% if product and product.sku %}{{ product.sku }}{% endif %}"
|
{{ ui::input(name="sku", id="sku", value=v_sku) }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="category_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="category", lang=lang | default(value='sk')) }}</label>
|
<label for="category_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="category", lang=lang | default(value='sk')) }}</label>
|
||||||
<select id="category_id" name="category_id"
|
<div class="relative">
|
||||||
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">
|
<select id="category_id" name="category_id"
|
||||||
<option value="">{{ t(key="no-category", lang=lang | default(value='sk')) }}</option>
|
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 category in categories %}
|
<option value="">{{ t(key="no-category", lang=lang | default(value='sk')) }}</option>
|
||||||
<option value="{{ category.id }}" {% if product and product.category_id == category.id %}selected{% endif %}>{{ category.name }}</option>
|
{% for category in categories %}
|
||||||
{% endfor %}
|
<option value="{{ category.id }}" {% if product and product.category_id == category.id %}selected{% endif %}>{{ category.name }}</option>
|
||||||
</select>
|
{% 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>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="slug" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="slug", lang=lang | default(value='sk')) }}</label>
|
<label for="slug" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="slug", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="slug" name="slug" type="text" value="{% if product %}{{ product.slug }}{% endif %}"
|
{{ ui::input(name="slug", id="slug", value=v_slug, placeholder=t(key='slug-auto', lang=lang | default(value='sk'))) }}
|
||||||
placeholder="{{ t(key='slug-auto', lang=lang | default(value='sk')) }}"
|
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="description" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="description", lang=lang | default(value='sk')) }}</label>
|
<label for="description" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="description", lang=lang | default(value='sk')) }}</label>
|
||||||
<textarea id="description" name="description" rows="5"
|
{{ ui::textarea(name="description", id="description", rows="5", value=v_desc) }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">{% if product and product.description %}{{ product.description }}{% endif %}</textarea>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
@@ -78,23 +78,14 @@
|
|||||||
{% if product and product.image %}
|
{% if product and product.image %}
|
||||||
<img src="/images/{{ product.image }}" alt="" class="size-24 rounded-radius object-cover">
|
<img src="/images/{{ product.image }}" alt="" class="size-24 rounded-radius object-cover">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="image" name="image" type="file" accept="image/*"
|
{{ ui::file_input(name="image", id="image", accept="image/*") }}
|
||||||
class="block w-full text-sm text-on-surface file:mr-3 file:rounded-radius file:border-0 file:bg-primary file:px-3 file:py-2 file:text-sm file:font-medium file:text-on-primary dark:text-on-surface-dark dark:file:bg-primary-dark dark:file:text-on-primary-dark">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="flex items-center gap-2">
|
{{ ui::checkbox(name="published", id="published", label=t(key="published", lang=lang | default(value='sk')), checked=v_pub) }}
|
||||||
<input type="checkbox" name="published" value="on" {% if product and product.published %}checked{% endif %}
|
|
||||||
class="size-4 rounded border-outline text-primary focus:ring-primary dark:border-outline-dark">
|
|
||||||
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="published", lang=lang | default(value='sk')) }}</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex gap-3 pt-2">
|
<div class="flex gap-3 pt-2">
|
||||||
<button type="submit"
|
{{ ui::button(label=t(key="save", lang=lang | default(value='sk')), 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">
|
{{ ui::button(variant="outline-secondary", label=t(key="cancel", lang=lang | default(value='sk')), href="/admin/catalog/products") }}
|
||||||
{{ 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>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ t(key="admin-products", lang=lang | default(value='sk')) }}{% endblock title %}
|
{% 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 %}
|
{% 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>
|
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-products", lang=lang | default(value='sk')) }}</h1>
|
||||||
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-products-desc", lang=lang | default(value='sk')) }}</p>
|
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-products-desc", lang=lang | default(value='sk')) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="/admin/catalog/products/new"
|
{{ ui::button(label=t(key="new-product", lang=lang | default(value='sk')), 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
|
<div class="mt-6 {{ ui::table_wrap_cls() }}">
|
||||||
{% if products | length > 0 %}
|
{% if products | length > 0 %}
|
||||||
<table class="w-full text-left text-sm">
|
<table class="{{ ui::table_cls() }}">
|
||||||
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
|
<thead class="{{ ui::thead_cls() }}">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="price", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="price", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="stock", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="stock", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="status", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="status", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 text-right font-semibold">{{ t(key="actions", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="actions", lang=lang | default(value='sk')), align="text-right") }}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-outline dark:divide-outline-dark">
|
<tbody class="{{ ui::tbody_cls() }}">
|
||||||
{% for product in products %}
|
{% for product in products %}
|
||||||
<tr class="hover:bg-surface-alt dark:hover:bg-surface-dark-alt">
|
<tr class="{{ ui::row_cls() }}">
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
{% if product.image %}
|
{% if product.image %}
|
||||||
@@ -47,20 +45,18 @@
|
|||||||
<td class="px-4 py-3 tabular-nums">{{ product.stock }}</td>
|
<td class="px-4 py-3 tabular-nums">{{ product.stock }}</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
{% if product.published %}
|
{% 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="flex flex-wrap justify-end gap-2">
|
<div class="flex flex-wrap justify-end gap-2">
|
||||||
<a href="/admin/catalog/products/{{ product.id }}/edit"
|
{{ ui::button(variant="outline-secondary", label=t(key="edit", lang=lang | default(value='sk')), href="/admin/catalog/products/" ~ product.id ~ "/edit", size="px-3 py-1.5 text-xs") }}
|
||||||
class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="edit", lang=lang | default(value='sk')) }}</a>
|
{{ ui::button(variant="outline-secondary", label=t(key="view", lang=lang | default(value='sk')), href="/shop/" ~ product.slug, size="px-3 py-1.5 text-xs") }}
|
||||||
<a href="/shop/{{ product.slug }}"
|
|
||||||
class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="view", lang=lang | default(value='sk')) }}</a>
|
|
||||||
<form method="post" action="/admin/catalog/products/{{ product.id }}/delete"
|
<form method="post" action="/admin/catalog/products/{{ product.id }}/delete"
|
||||||
onsubmit="return confirm('{{ t(key="confirm-delete", lang=lang | default(value='sk')) }}')">
|
onsubmit="return confirm('{{ t(key="confirm-delete", lang=lang | default(value='sk')) }}')">
|
||||||
<button type="submit" class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-danger transition hover:bg-danger/10 dark:border-outline-dark">{{ t(key="delete", lang=lang | default(value='sk')) }}</button>
|
{{ ui::button(variant="outline-danger", label=t(key="delete", lang=lang | default(value='sk')), type="submit", size="px-3 py-1.5 text-xs") }}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -71,10 +67,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="flex flex-col items-center gap-3 px-4 py-16 text-center">
|
<div class="flex flex-col items-center gap-3 px-4 py-16 text-center">
|
||||||
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-no-products", lang=lang | default(value='sk')) }}</p>
|
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="admin-no-products", lang=lang | default(value='sk')) }}</p>
|
||||||
<a href="/admin/catalog/products/new"
|
{{ ui::button(label=t(key="new-product", lang=lang | default(value='sk')), 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>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 %}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ t(key="admin-orders", lang=lang | default(value='sk')) }}{% endblock title %}
|
{% 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 %}
|
{% block crumb %}{{ t(key="admin-orders", lang=lang | default(value='sk')) }}{% endblock crumb %}
|
||||||
@@ -6,29 +7,29 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-orders", lang=lang | default(value='sk')) }}</h1>
|
<h1 class="text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="admin-orders", lang=lang | default(value='sk')) }}</h1>
|
||||||
|
|
||||||
<div class="mt-6 overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
|
<div class="mt-6 {{ ui::table_wrap_cls() }}">
|
||||||
{% if orders | length > 0 %}
|
{% if orders | length > 0 %}
|
||||||
<table class="w-full text-left text-sm">
|
<table class="{{ ui::table_cls() }}">
|
||||||
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
|
<thead class="{{ ui::thead_cls() }}">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="order-number", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="order-number", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="order-customer", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="order-customer", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="order-status", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="order-status", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 text-right font-semibold">{{ t(key="order-total", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="order-total", lang=lang | default(value='sk')), align="text-right") }}
|
||||||
<th class="px-4 py-3"></th>
|
{{ ui::th(label="") }}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-outline dark:divide-outline-dark">
|
<tbody class="{{ ui::tbody_cls() }}">
|
||||||
{% for order in orders %}
|
{% for order in orders %}
|
||||||
<tr class="hover:bg-surface-alt dark:hover:bg-surface-dark-alt">
|
<tr class="{{ ui::row_cls() }}">
|
||||||
<td class="px-4 py-3 font-mono font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.order_number }}</td>
|
<td class="px-4 py-3 font-mono font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.order_number }}</td>
|
||||||
<td class="px-4 py-3">{{ order.email }}</td>
|
<td class="px-4 py-3">{{ order.email }}</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<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>
|
||||||
<td class="px-4 py-3 text-right tabular-nums">{{ order.total }} {{ order.currency }}</td>
|
<td class="px-4 py-3 text-right tabular-nums">{{ order.total }} {{ order.currency }}</td>
|
||||||
<td class="px-4 py-3 text-right">
|
<td class="px-4 py-3 text-right">
|
||||||
<a href="/admin/orders/{{ order.id }}" class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="view", lang=lang | default(value='sk')) }}</a>
|
{{ ui::button(variant="outline-secondary", label=t(key="view", lang=lang | default(value='sk')), href="/admin/orders/" ~ order.id, size="px-3 py-1.5 text-xs") }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ order.order_number }}{% endblock title %}
|
{% block title %}{{ order.order_number }}{% endblock title %}
|
||||||
{% block crumb %}{{ t(key="admin-orders", lang=lang | default(value='sk')) }}{% endblock crumb %}
|
{% block crumb %}{{ t(key="admin-orders", lang=lang | default(value='sk')) }}{% endblock crumb %}
|
||||||
@@ -6,27 +7,25 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h1 class="font-mono text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.order_number }}</h1>
|
<h1 class="font-mono text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.order_number }}</h1>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{% if ship_error %}
|
{% 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">
|
{{ ui::alert_danger(message=ship_error, extra="mt-4") }}
|
||||||
{{ ship_error }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="mt-6 grid gap-6 lg:grid-cols-3">
|
<div class="mt-6 grid gap-6 lg:grid-cols-3">
|
||||||
<div class="space-y-6 lg:col-span-2">
|
<div class="space-y-6 lg:col-span-2">
|
||||||
<div class="overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
|
<div class="{{ ui::table_wrap_cls() }}">
|
||||||
<table class="w-full text-left text-sm">
|
<table class="{{ ui::table_cls() }}">
|
||||||
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
|
<thead class="{{ ui::thead_cls() }}">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="quantity", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="quantity", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 text-right font-semibold">{{ t(key="order-total", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="order-total", lang=lang | default(value='sk')), align="text-right") }}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-outline dark:divide-outline-dark">
|
<tbody class="{{ ui::tbody_cls() }}">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-4 py-3">{{ item.product_name }}</td>
|
<td class="px-4 py-3">{{ item.product_name }}</td>
|
||||||
@@ -35,7 +34,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot class="border-t border-outline dark:border-outline-dark">
|
<tfoot class="{{ ui::tfoot_cls() }}">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="px-4 py-3 text-right font-semibold">{{ t(key="order-total", lang=lang | default(value='sk')) }}</td>
|
<td colspan="2" class="px-4 py-3 text-right font-semibold">{{ t(key="order-total", lang=lang | default(value='sk')) }}</td>
|
||||||
<td class="px-4 py-3 text-right font-bold tabular-nums text-primary dark:text-primary-dark">{{ order.total }} {{ order.currency }}</td>
|
<td class="px-4 py-3 text-right font-bold tabular-nums text-primary dark:text-primary-dark">{{ order.total }} {{ order.currency }}</td>
|
||||||
@@ -53,6 +52,15 @@
|
|||||||
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.email }}</p>
|
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.email }}</p>
|
||||||
{% if order.phone %}<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.phone }}</p>{% endif %}
|
{% if order.phone %}<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.phone }}</p>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if order.account_type == "company" %}
|
||||||
|
<div>
|
||||||
|
<p class="text-xs uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-company-details", lang=lang | default(value='sk')) }}</p>
|
||||||
|
<p class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.company_name }}</p>
|
||||||
|
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ t(key="company-ico", lang=lang | default(value='sk')) }}: {{ order.company_id }}</p>
|
||||||
|
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ t(key="company-dic", lang=lang | default(value='sk')) }}: {{ order.tax_id }}</p>
|
||||||
|
{% if order.vat_id %}<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ t(key="company-icdph", lang=lang | default(value='sk')) }}: {{ order.vat_id }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</p>
|
<p class="text-xs uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</p>
|
||||||
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.address }}<br>{{ order.zip }} {{ order.city }}<br>{{ order.country }}</p>
|
<p class="text-on-surface/80 dark:text-on-surface-dark/80">{{ order.address }}<br>{{ order.zip }} {{ order.city }}<br>{{ order.country }}</p>
|
||||||
@@ -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>
|
{{ t(key="order-tracking", lang=lang | default(value='sk')) }}: <span class="font-mono font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ order.tracking_number }}</span>
|
||||||
</p>
|
</p>
|
||||||
{% if order.label_url %}
|
{% if order.label_url %}
|
||||||
<a href="{{ order.label_url }}" target="_blank" rel="noopener"
|
{{ ui::button(variant="outline-secondary", label=t(key="order-label", lang=lang | default(value='sk')), href=order.label_url, size="px-3 py-1.5 text-xs", attrs='target="_blank" rel="noopener"') }}
|
||||||
class="inline-flex items-center rounded-radius border border-outline px-3 py-1.5 text-xs font-medium text-on-surface transition hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">{{ t(key="order-label", lang=lang | default(value='sk')) }}</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif carrier == "none" %}
|
{% elif carrier == "none" %}
|
||||||
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-manual-fulfillment", lang=lang | default(value='sk')) }}</p>
|
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-manual-fulfillment", lang=lang | default(value='sk')) }}</p>
|
||||||
@@ -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>
|
<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"
|
<form method="post" action="/admin/orders/{{ order.id }}/ship"
|
||||||
onsubmit="return confirm('{{ t(key="order-send-confirm", lang=lang | default(value='sk')) }}')">
|
onsubmit="return confirm('{{ t(key="order-send-confirm", lang=lang | default(value='sk')) }}')">
|
||||||
<button type="submit"
|
{% set carrier_up = carrier | upper %}
|
||||||
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">
|
{% set ship_label = t(key="order-send-to-carrier", lang=lang | default(value='sk')) ~ " " ~ carrier_up %}
|
||||||
{{ t(key="order-send-to-carrier", lang=lang | default(value='sk')) }} {{ carrier | upper }}
|
{{ ui::button(label=ship_label, type="submit", extra="w-full") }}
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/admin/orders/{{ order.id }}/status" class="space-y-3 rounded-radius border border-outline bg-surface p-5 dark:border-outline-dark dark:bg-surface-dark-alt">
|
<form method="post" action="/admin/orders/{{ order.id }}/status" class="space-y-3 rounded-radius border border-outline bg-surface p-5 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
<label for="status" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="order-status", lang=lang | default(value='sk')) }}</label>
|
<label for="status" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="order-status", lang=lang | default(value='sk')) }}</label>
|
||||||
<select id="status" name="status"
|
<div class="relative">
|
||||||
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">
|
<select id="status" name="status"
|
||||||
{% for status in statuses %}
|
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="{{ status }}" {% if order.status == status %}selected{% endif %}>{{ t(key="order-status-" ~ status, lang=lang | default(value='sk')) }}</option>
|
{% for status in statuses %}
|
||||||
{% endfor %}
|
<option value="{{ status }}" {% if order.status == status %}selected{% endif %}>{{ t(key="order-status-" ~ status, lang=lang | default(value='sk')) }}</option>
|
||||||
</select>
|
{% endfor %}
|
||||||
<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>
|
</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>
|
</form>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ t(key="admin-shipping", lang=lang | default(value='sk')) }}{% endblock title %}
|
{% 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 %}
|
{% block crumb %}{{ t(key="admin-shipping", lang=lang | default(value='sk')) }}{% endblock crumb %}
|
||||||
@@ -19,18 +20,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="price-{{ method.id }}" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="price", lang=lang | default(value='sk')) }}</label>
|
<label for="price-{{ method.id }}" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="price", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="price-{{ method.id }}" name="price" type="text" inputmode="decimal" value="{{ method.price }}"
|
{{ ui::input(name="price", id="price-" ~ method.id, value=method.price, width="w-28", attrs='inputmode="decimal"') }}
|
||||||
class="w-28 rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
<label class="flex items-center gap-2 pb-2">
|
<div class="pb-2">{{ ui::checkbox(name="enabled", label=t(key="shipping-enabled", lang=lang | default(value='sk')), checked=method.enabled) }}</div>
|
||||||
<input type="checkbox" name="enabled" value="on" {% if method.enabled %}checked{% endif %}
|
{{ ui::button(label=t(key="save", lang=lang | default(value='sk')), type="submit", extra="ml-auto") }}
|
||||||
class="size-4 rounded border-outline text-primary focus:ring-primary dark:border-outline-dark">
|
|
||||||
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="shipping-enabled", lang=lang | default(value='sk')) }}</span>
|
|
||||||
</label>
|
|
||||||
<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>
|
|
||||||
</form>
|
</form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
67
assets/views/auth/login.html
Normal file
67
assets/views/auth/login.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
|
{% block title %}{{ t(key="login-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mx-auto mt-8 max-w-sm">
|
||||||
|
<div
|
||||||
|
class="rounded-radius border border-outline bg-surface-alt shadow-sm dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between border-b border-outline px-5 py-3 dark:border-outline-dark">
|
||||||
|
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
|
{{ t(key="brand", lang=lang | default(value='sk')) }}
|
||||||
|
</span>
|
||||||
|
{{ ui::badge(label=t(key="auth", lang=lang | default(value='sk')), variant="primary") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-5">
|
||||||
|
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
|
{{ t(key="login-title", lang=lang | default(value='sk')) }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{% if error == "unverified" %}
|
||||||
|
{{ ui::alert_danger(message=t(key="login-error-unverified", lang=lang | default(value='sk')), extra="mt-3") }}
|
||||||
|
<p class="mt-2 text-sm text-on-surface dark:text-on-surface-dark">
|
||||||
|
<a href="/resend-verification" class="font-medium text-primary underline-offset-2 hover:underline dark:text-primary-dark">{{ t(key="login-resend", lang=lang | default(value='sk')) }}</a>
|
||||||
|
</p>
|
||||||
|
{% elif error %}
|
||||||
|
{{ ui::alert_danger(message=t(key="login-error", lang=lang | default(value='sk')), extra="mt-3") }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/login" hx-boost="false" class="mt-4 flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="email"
|
||||||
|
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
|
{{ t(key="login-email", lang=lang | default(value='sk')) }}
|
||||||
|
</label>
|
||||||
|
{{ ui::input(name="email", id="email", type="email", required=true, autocomplete="email", attrs="autofocus") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="password"
|
||||||
|
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
|
{{ t(key="login-password", lang=lang | default(value='sk')) }}
|
||||||
|
</label>
|
||||||
|
{{ ui::input(name="password", id="password", type="password", required=true, autocomplete="current-password") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ ui::button(label=t(key="login-auth", lang=lang | default(value='sk')), type="submit", extra="mt-1 w-full") }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-5 flex items-center gap-3 text-xs text-on-surface/50 dark:text-on-surface-dark/50">
|
||||||
|
<span class="h-px flex-1 bg-outline dark:bg-outline-dark"></span>
|
||||||
|
{{ t(key="auth-or", lang=lang | default(value='sk')) }}
|
||||||
|
<span class="h-px flex-1 bg-outline dark:bg-outline-dark"></span>
|
||||||
|
</div>
|
||||||
|
{{ ui::button(label=t(key="auth-google", lang=lang | default(value='sk')), href="/api/oauth2/google", variant="outline-secondary", attrs='hx-boost="false"', extra="mt-4 w-full", icon='<svg class="size-4" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="#FFC107" d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z"/><path fill="#FF3D00" d="m6.306 14.691 6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z"/><path fill="#4CAF50" d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.91 11.91 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z"/><path fill="#1976D2" d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002 6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z"/></svg>') }}
|
||||||
|
|
||||||
|
<p class="mt-4 text-sm text-on-surface dark:text-on-surface-dark">
|
||||||
|
{{ t(key="login-no-account", lang=lang | default(value='sk')) }}
|
||||||
|
<a href="/register"
|
||||||
|
class="font-medium text-primary underline-offset-2 hover:underline dark:text-primary-dark">{{ t(key="nav-register", lang=lang | default(value='sk')) }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
96
assets/views/auth/register.html
Normal file
96
assets/views/auth/register.html
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
|
{% block title %}{{ t(key="register-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mx-auto mt-8 max-w-sm">
|
||||||
|
<div
|
||||||
|
class="rounded-radius border border-outline bg-surface-alt shadow-sm dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between border-b border-outline px-5 py-3 dark:border-outline-dark">
|
||||||
|
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
|
{{ t(key="brand", lang=lang | default(value='sk')) }}
|
||||||
|
</span>
|
||||||
|
{{ ui::badge(label=t(key="auth", lang=lang | default(value='sk')), variant="primary") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-5">
|
||||||
|
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
|
{{ t(key="register-title", lang=lang | default(value='sk')) }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{% if error == "exists" %}
|
||||||
|
{{ ui::alert_danger(message=t(key="register-error-exists", lang=lang | default(value='sk')), extra="mt-3") }}
|
||||||
|
{% elif error == "mismatch" %}
|
||||||
|
{{ ui::alert_danger(message=t(key="set-password-mismatch", lang=lang | default(value='sk')), extra="mt-3") }}
|
||||||
|
{% elif error == "weak" %}
|
||||||
|
{{ ui::alert_danger(message=t(key="set-password-weak", lang=lang | default(value='sk')), extra="mt-3") }}
|
||||||
|
{% elif error %}
|
||||||
|
{{ ui::alert_danger(message=t(key="register-error-invalid", lang=lang | default(value='sk')), extra="mt-3") }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/register" hx-boost="false" class="mt-4 flex flex-col gap-4"
|
||||||
|
x-data="{ password: '', confirm: '' }">
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-type", lang=lang | default(value='sk')) }}</span>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<label class="flex cursor-pointer items-center gap-2 rounded-radius border border-outline px-3 py-2 text-sm transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
||||||
|
{{ ui::radio(name="account_type", value="personal", checked=true) }}
|
||||||
|
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-personal", lang=lang | default(value='sk')) }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex cursor-pointer items-center gap-2 rounded-radius border border-outline px-3 py-2 text-sm transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
||||||
|
{{ ui::radio(name="account_type", value="company") }}
|
||||||
|
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company", lang=lang | default(value='sk')) }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-type-locked", lang=lang | default(value='sk')) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="email"
|
||||||
|
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
|
{{ t(key="login-email", lang=lang | default(value='sk')) }}
|
||||||
|
</label>
|
||||||
|
{{ ui::input(name="email", id="email", type="email", required=true, autocomplete="email", attrs="autofocus") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="password"
|
||||||
|
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
|
{{ t(key="login-password", lang=lang | default(value='sk')) }}
|
||||||
|
</label>
|
||||||
|
{{ ui::input(name="password", id="password", type="password", required=true, autocomplete="new-password", attrs='x-model="password"') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="password_confirm"
|
||||||
|
class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
|
{{ t(key="set-password-confirm", lang=lang | default(value='sk')) }}
|
||||||
|
</label>
|
||||||
|
{{ ui::input(name="password_confirm", id="password_confirm", type="password", required=true, autocomplete="new-password", attrs='x-model="confirm"') }}
|
||||||
|
<span x-cloak x-show="confirm.length > 0 && password !== confirm"
|
||||||
|
class="text-xs text-danger dark:text-danger">
|
||||||
|
{{ t(key="set-password-mismatch", lang=lang | default(value='sk')) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ ui::button(label=t(key="register-submit", lang=lang | default(value='sk')), type="submit", extra="mt-1 w-full", attrs=':disabled="password !== confirm"') }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-5 flex items-center gap-3 text-xs text-on-surface/50 dark:text-on-surface-dark/50">
|
||||||
|
<span class="h-px flex-1 bg-outline dark:bg-outline-dark"></span>
|
||||||
|
{{ t(key="auth-or", lang=lang | default(value='sk')) }}
|
||||||
|
<span class="h-px flex-1 bg-outline dark:bg-outline-dark"></span>
|
||||||
|
</div>
|
||||||
|
{{ ui::button(label=t(key="auth-google", lang=lang | default(value='sk')), href="/api/oauth2/google", variant="outline-secondary", attrs='hx-boost="false"', extra="mt-4 w-full", icon='<svg class="size-4" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="#FFC107" d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z"/><path fill="#FF3D00" d="m6.306 14.691 6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z"/><path fill="#4CAF50" d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.91 11.91 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z"/><path fill="#1976D2" d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002 6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z"/></svg>') }}
|
||||||
|
|
||||||
|
<p class="mt-4 text-sm text-on-surface dark:text-on-surface-dark">
|
||||||
|
{{ t(key="login-have-account", lang=lang | default(value='sk')) }}
|
||||||
|
<a href="/login"
|
||||||
|
class="font-medium text-primary underline-offset-2 hover:underline dark:text-primary-dark">{{ t(key="nav-login", lang=lang | default(value='sk')) }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
37
assets/views/auth/resend_verification.html
Normal file
37
assets/views/auth/resend_verification.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
|
{% block title %}{{ t(key="resend-verification-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mx-auto mt-8 max-w-sm">
|
||||||
|
<div class="rounded-radius border border-outline bg-surface-alt shadow-sm dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<div class="flex items-center justify-between border-b border-outline px-5 py-3 dark:border-outline-dark">
|
||||||
|
<span class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="brand", lang=lang | default(value='sk')) }}</span>
|
||||||
|
{{ ui::badge(label=t(key="auth", lang=lang | default(value='sk')), variant="primary") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-5">
|
||||||
|
<h1 class="text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="resend-verification-title", lang=lang | default(value='sk')) }}</h1>
|
||||||
|
|
||||||
|
{% if done %}
|
||||||
|
<div class="mt-3 rounded-radius border border-success bg-success/10 px-4 py-3 text-sm text-success" role="status">
|
||||||
|
{{ t(key="resend-verification-done", lang=lang | default(value='sk')) }}
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-sm text-on-surface dark:text-on-surface-dark">
|
||||||
|
<a href="/login" class="font-medium text-primary underline-offset-2 hover:underline dark:text-primary-dark">{{ t(key="nav-login", lang=lang | default(value='sk')) }}</a>
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="mt-1 text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="resend-verification-intro", lang=lang | default(value='sk')) }}</p>
|
||||||
|
<form method="post" action="/resend-verification" hx-boost="false" class="mt-4 flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="email" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="login-email", lang=lang | default(value='sk')) }}</label>
|
||||||
|
{{ ui::input(name="email", id="email", type="email", required=true, autocomplete="email", attrs="autofocus") }}
|
||||||
|
</div>
|
||||||
|
{{ ui::button(label=t(key="resend-verification-submit", lang=lang | default(value='sk')), type="submit", extra="mt-1 w-full") }}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
47
assets/views/auth/set_password.html
Normal file
47
assets/views/auth/set_password.html
Normal 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 %}
|
||||||
27
assets/views/auth/verified.html
Normal file
27
assets/views/auth/verified.html
Normal 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 %}
|
||||||
20
assets/views/auth/verify_sent.html
Normal file
20
assets/views/auth/verify_sent.html
Normal 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 %}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="{{ lang | default(value='sk') }}" data-theme="dark">
|
<html lang="{{ lang | default(value='sk') }}" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
@@ -47,6 +48,12 @@
|
|||||||
if (!v) return 0;
|
if (!v) return 0;
|
||||||
return v.split(',').reduce(function (s, e) { return s + (parseInt(e.split(':')[1]) || 0) }, 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>
|
</script>
|
||||||
<link href="/static/css/app.css?v=2026-06-16" rel="stylesheet" type="text/css">
|
<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>
|
<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">
|
class="sticky top-0 z-30 border-b border-outline bg-surface/95 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95">
|
||||||
<nav x-data="{ mobile: false }" class="mx-auto flex max-w-7xl items-center gap-4 px-4 py-3">
|
<nav x-data="{ mobile: false }" class="mx-auto flex max-w-7xl items-center gap-4 px-4 py-3">
|
||||||
<!-- category sidebar toggle (mobile only) -->
|
<!-- category sidebar toggle (mobile only) -->
|
||||||
<button type="button" @click="cats = !cats" :aria-expanded="cats"
|
{% set hamburger_icon = ui::icon(name="hamburger", size="size-6") %}
|
||||||
aria-label="{{ t(key='categories', lang=lang | default(value='sk')) }}"
|
{{ ui::icon_button(aria_label=t(key='categories', lang=lang | default(value='sk')), attrs='@click="cats = !cats" :aria-expanded="cats"', extra="lg:hidden", icon=hamburger_icon) }}
|
||||||
class="inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt lg:hidden dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<a href="/"
|
<a href="/"
|
||||||
class="text-lg font-bold tracking-tight text-on-surface-strong dark:text-on-surface-dark-strong">
|
class="text-lg font-bold tracking-tight text-on-surface-strong dark:text-on-surface-dark-strong">
|
||||||
{{ t(key="brand", lang=lang | default(value='sk')) }}
|
{{ t(key="brand", lang=lang | default(value='sk')) }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- desktop links -->
|
<!-- desktop links — Penguin navbar link treatment via ui::nav_link -->
|
||||||
<ul class="ml-2 hidden items-center gap-1 md:flex">
|
<ul class="ml-2 hidden items-center gap-6 md:flex">
|
||||||
<li><a href="/" data-nav="/" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li>
|
<li>{{ ui::nav_link(label=t(key="nav-home", lang=lang | default(value='sk')), href="/", data_nav="/") }}</li>
|
||||||
<li><a href="/shop" data-nav="/shop" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li>
|
<li>{{ ui::nav_link(label=t(key="nav-shop", lang=lang | default(value='sk')), href="/shop", data_nav="/shop") }}</li>
|
||||||
{% if logged_in_admin %}
|
{% if logged_in_admin %}
|
||||||
<li><a href="/admin/dashboard" hx-boost="false" data-nav="/admin" class="rounded-radius px-3 py-1.5 text-sm font-medium text-warning transition hover:bg-surface-alt dark:hover:bg-surface-dark-alt">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li>
|
<li>{{ ui::nav_link(label=t(key="admin-title", lang=lang | default(value='sk')), href="/admin/dashboard", data_nav="/admin", variant="warning", attrs='hx-boost="false"') }}</li>
|
||||||
<li>
|
<li>
|
||||||
<form method="post" action="/admin/logout" hx-boost="false">
|
<form method="post" action="/logout" hx-boost="false">
|
||||||
<button type="submit" class="rounded-radius px-3 py-1.5 text-sm font-medium text-danger transition hover:bg-surface-alt dark:hover:bg-surface-dark-alt">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
|
<button type="submit" class="text-sm font-medium text-danger underline-offset-2 transition hover:opacity-75 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% elif logged_in_customer %}
|
||||||
|
<li>{{ ui::nav_link(label=t(key="nav-profile", lang=lang | default(value='sk')), href="/account/profile", data_nav="/account") }}</li>
|
||||||
|
<li>
|
||||||
|
<form method="post" action="/logout" hx-boost="false">
|
||||||
|
<button type="submit" class="text-sm font-medium text-danger underline-offset-2 transition hover:opacity-75 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="/admin/login" data-nav="/admin/login" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li>
|
<li>{{ ui::nav_link(label=t(key="nav-login", lang=lang | default(value='sk')), href="/login", data_nav="/login") }}</li>
|
||||||
|
<li>{{ ui::nav_link(label=t(key="nav-register", lang=lang | default(value='sk')), href="/register", data_nav="/register") }}</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -93,95 +103,51 @@
|
|||||||
<!-- cart with live item-count badge read from the `cart` cookie -->
|
<!-- cart with live item-count badge read from the `cart` cookie -->
|
||||||
<a href="/cart" data-nav="/cart"
|
<a href="/cart" data-nav="/cart"
|
||||||
x-data="{ count: 0 }"
|
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')) }}"
|
aria-label="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
|
||||||
title="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
|
title="{{ t(key='cart-title', lang=lang | default(value='sk')) }}"
|
||||||
class="relative inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">
|
class="relative inline-flex size-9 shrink-0 items-center justify-center rounded-radius bg-transparent text-secondary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 dark:text-secondary-dark dark:focus-visible:outline-secondary-dark">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
{{ ui::icon(name="cart") }}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
|
|
||||||
</svg>
|
|
||||||
<span x-show="count > 0" x-cloak x-text="count"
|
<span x-show="count > 0" x-cloak x-text="count"
|
||||||
class="absolute -right-1 -top-1 inline-flex min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-4 text-on-primary dark:bg-primary-dark dark:text-on-primary-dark"></span>
|
class="absolute -right-1 -top-1 inline-flex min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-4 text-on-primary dark:bg-primary-dark dark:text-on-primary-dark"></span>
|
||||||
</a>
|
</a>
|
||||||
<!-- settings (language + theme) dropdown -->
|
<!-- settings (language + theme) dropdown -->
|
||||||
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative">
|
<div x-data="{ open: false }" @keydown.escape="open = false" class="relative">
|
||||||
<button type="button" @click="open = !open" :aria-expanded="open"
|
{% include "partials/settings_dropdown.html" %}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- mobile hamburger -->
|
<!-- mobile hamburger — Penguin animated icon swap (bars ↔ X), kept in
|
||||||
<button type="button" @click="mobile = !mobile" :aria-expanded="mobile"
|
our ghost-square icon-button shell for consistency with cart/gear -->
|
||||||
aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
|
<button type="button" @click="mobile = !mobile" :aria-expanded="mobile" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
|
||||||
class="inline-flex size-9 items-center justify-center rounded-radius text-on-surface transition hover:bg-surface-alt md:hidden dark:text-on-surface-dark dark:hover:bg-surface-dark-alt">
|
class="inline-flex size-9 shrink-0 items-center justify-center rounded-radius bg-transparent text-secondary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 md:hidden dark:text-secondary-dark dark:focus-visible:outline-secondary-dark">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
{{ ui::icon(name="hamburger", size="size-6", attrs='x-show="!mobile"') }}
|
||||||
stroke="currentColor" class="size-6">
|
{{ ui::icon(name="close", size="size-6", attrs='x-cloak x-show="mobile"') }}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- mobile menu panel -->
|
<!-- mobile menu panel — Penguin sidebar-style menu rows (hover:bg-primary/5,
|
||||||
|
underline focus), active state via data-nav + markActiveNav() -->
|
||||||
<ul x-show="mobile" x-cloak @click.outside="mobile = false" x-transition
|
<ul x-show="mobile" x-cloak @click.outside="mobile = false" x-transition
|
||||||
class="absolute inset-x-0 top-full mx-4 mt-2 flex flex-col gap-1 rounded-radius border border-outline bg-surface p-2 shadow-lg md:hidden dark:border-outline-dark dark:bg-surface-dark-alt">
|
class="absolute inset-x-0 top-full mx-4 mt-2 flex flex-col gap-1 rounded-radius border border-outline bg-surface p-2 shadow-lg md:hidden dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
<li><a href="/" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li>
|
<li><a href="/" data-nav="/" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li>
|
||||||
<li><a href="/shop" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li>
|
<li><a href="/shop" data-nav="/shop" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li>
|
||||||
{% if logged_in_admin %}
|
{% if logged_in_admin %}
|
||||||
<li><a href="/admin/dashboard" hx-boost="false" class="block rounded-radius px-3 py-2 text-sm font-medium text-warning hover:bg-surface-alt dark:hover:bg-surface-dark">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li>
|
<li><a href="/admin/dashboard" hx-boost="false" data-nav="/admin" class="block rounded-radius px-3 py-2 text-sm font-medium text-warning underline-offset-2 transition hover:bg-primary/5 focus:outline-hidden focus-visible:underline">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li>
|
||||||
<li>
|
<li>
|
||||||
<form method="post" action="/admin/logout" hx-boost="false">
|
<form method="post" action="/logout" hx-boost="false">
|
||||||
<button type="submit" class="block w-full rounded-radius px-3 py-2 text-left text-sm font-medium text-danger hover:bg-surface-alt dark:hover:bg-surface-dark">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
|
<button type="submit" class="block w-full rounded-radius px-3 py-2 text-left text-sm font-medium text-danger underline-offset-2 transition hover:bg-primary/5 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% elif logged_in_customer %}
|
||||||
|
<li><a href="/account/profile" data-nav="/account" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-profile", lang=lang | default(value='sk')) }}</a></li>
|
||||||
|
<li>
|
||||||
|
<form method="post" action="/logout" hx-boost="false">
|
||||||
|
<button type="submit" class="block w-full rounded-radius px-3 py-2 text-left text-sm font-medium text-danger underline-offset-2 transition hover:bg-primary/5 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="/admin/login" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li>
|
<li><a href="/login" data-nav="/login" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-login", lang=lang | default(value='sk')) }}</a></li>
|
||||||
|
<li><a href="/register" data-nav="/register" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-register", lang=lang | default(value='sk')) }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -205,5 +171,151 @@
|
|||||||
{% block content %}{% endblock content %}
|
{% block content %}{% endblock content %}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ t(key="brand", lang=lang | default(value='sk')) }}{% endblock title %}
|
{% block title %}{{ t(key="brand", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||||
|
|
||||||
|
|||||||
215
assets/views/macros/ui.html
Normal file
215
assets/views/macros/ui.html
Normal 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 %}
|
||||||
56
assets/views/partials/settings_dropdown.html
Normal file
56
assets/views/partials/settings_dropdown.html
Normal 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>
|
||||||
@@ -1,29 +1,37 @@
|
|||||||
<div
|
{# Adapted from the vendored Penguin UI component
|
||||||
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">
|
(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">
|
<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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-col gap-1 p-4 pb-2">
|
<!-- Content -->
|
||||||
<h3 class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ product.name }}</h3>
|
<div class="flex flex-1 flex-col gap-1 p-6 pb-2">
|
||||||
<p class="mt-auto pt-2 font-semibold text-primary dark:text-primary-dark">{{ product.price }} {{ product.currency }}</p>
|
<!-- 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>
|
</div>
|
||||||
</a>
|
</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 %}
|
{% 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>
|
<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="product_id" value="{{ product.id }}">
|
||||||
<input type="hidden" name="quantity" value="1">
|
<input type="hidden" name="quantity" value="1">
|
||||||
<button type="submit"
|
{{ ui::button(label=t(key="add-to-cart", lang=lang | default(value='sk')), type="submit", extra="w-full", icon='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="size-3.5"><path fill-rule="evenodd" d="M5 4a3 3 0 0 1 6 0v1h.643a1.5 1.5 0 0 1 1.492 1.35l.7 7A1.5 1.5 0 0 1 12.342 15H3.657a1.5 1.5 0 0 1-1.492-1.65l.7-7A1.5 1.5 0 0 1 4.357 5H5V4Zm4.5 0v1h-3V4a1.5 1.5 0 0 1 3 0Zm-3 3.75a.75.75 0 0 0-1.5 0v1a3 3 0 1 0 6 0v-1a.75.75 0 0 0-1.5 0v1a1.5 1.5 0 1 1-3 0v-1Z" clip-rule="evenodd" /></svg>') }}
|
||||||
class="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>
|
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="inline-flex justify-center rounded-radius bg-danger/10 px-3 py-2 text-xs font-medium text-danger">{{ t(key="out-of-stock", lang=lang | default(value='sk')) }}</p>
|
<p class="inline-flex justify-center rounded-radius bg-danger/10 px-3 py-2 text-xs font-medium text-danger">{{ t(key="out-of-stock", lang=lang | default(value='sk')) }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</article>
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
{# Cart contents, swapped in via htmx on quantity change / removal so the page
|
{# 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
|
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. #}
|
and returned on its own by /cart/update and /cart/remove. #}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
{% if items | length > 0 %}
|
{% if items | length > 0 %}
|
||||||
<div class="overflow-hidden rounded-radius border border-outline dark:border-outline-dark">
|
<div class="{{ ui::table_wrap_cls() }}">
|
||||||
<table class="w-full text-left text-sm">
|
<table class="{{ ui::table_cls() }}">
|
||||||
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70 dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark/70">
|
<thead class="{{ ui::thead_cls() }}">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="product", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="price", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="price", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 font-semibold">{{ t(key="quantity", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="quantity", lang=lang | default(value='sk'))) }}
|
||||||
<th class="px-4 py-3 text-right font-semibold">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</th>
|
{{ ui::th(label=t(key="cart-total", lang=lang | default(value='sk')), align="text-right") }}
|
||||||
<th class="px-4 py-3"></th>
|
{{ ui::th(label="") }}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-outline dark:divide-outline-dark">
|
<tbody class="{{ ui::tbody_cls() }}">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
$el.dispatchEvent(new Event('cartchange', { bubbles: true }));
|
$el.dispatchEvent(new Event('cartchange', { bubbles: true }));
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
class="w-20 rounded-radius border border-outline bg-surface px-2 py-1 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
class="w-20 rounded-radius border border-outline bg-surface-alt px-2 py-1 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-75 dark:border-outline-dark dark:bg-surface-dark-alt/50 dark:text-on-surface-dark dark:focus-visible:outline-primary-dark">
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-right font-medium tabular-nums">{{ item.line_total }} {{ item.currency }}</td>
|
<td class="px-4 py-3 text-right font-medium tabular-nums">{{ item.line_total }} {{ item.currency }}</td>
|
||||||
@@ -43,13 +44,13 @@
|
|||||||
<form method="post" action="/cart/remove"
|
<form method="post" action="/cart/remove"
|
||||||
hx-post="/cart/remove" hx-target="#cart-body" hx-swap="innerHTML">
|
hx-post="/cart/remove" hx-target="#cart-body" hx-swap="innerHTML">
|
||||||
<input type="hidden" name="product_id" value="{{ item.id }}">
|
<input type="hidden" name="product_id" value="{{ item.id }}">
|
||||||
<button type="submit" class="text-xs font-medium text-danger hover:underline">{{ t(key="cart-remove", lang=lang | default(value='sk')) }}</button>
|
{{ ui::button(variant="ghost-danger", label=t(key="cart-remove", lang=lang | default(value='sk')), type="submit", size="px-2 py-1 text-xs") }}
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot class="border-t border-outline dark:border-outline-dark">
|
<tfoot class="{{ ui::tfoot_cls() }}">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="px-4 py-3 text-right font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</td>
|
<td colspan="3" class="px-4 py-3 text-right font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="cart-total", lang=lang | default(value='sk')) }}</td>
|
||||||
<td class="px-4 py-3 text-right text-lg font-bold tabular-nums text-primary dark:text-primary-dark">{{ total }} {{ currency }}</td>
|
<td class="px-4 py-3 text-right text-lg font-bold tabular-nums text-primary dark:text-primary-dark">{{ total }} {{ currency }}</td>
|
||||||
@@ -60,12 +61,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex flex-wrap justify-between gap-3">
|
<div class="mt-6 flex flex-wrap justify-between gap-3">
|
||||||
<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>
|
{{ ui::button(variant="outline-secondary", label=t(key="cart-continue", lang=lang | default(value='sk')), href="/shop") }}
|
||||||
<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(label=t(key="cart-checkout", lang=lang | default(value='sk')), href="/checkout", size="px-5 py-2 text-sm") }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="rounded-radius border border-outline px-6 py-16 text-center dark:border-outline-dark">
|
<div class="rounded-radius border border-outline px-6 py-16 text-center dark:border-outline-dark">
|
||||||
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="cart-empty", lang=lang | default(value='sk')) }}</p>
|
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="cart-empty", lang=lang | default(value='sk')) }}</p>
|
||||||
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -4,57 +4,59 @@
|
|||||||
with children is expandable (accordion); one without is a plain link.
|
with children is expandable (accordion); one without is a plain link.
|
||||||
Active state is set client-side by markActiveNav() via data-nav +
|
Active state is set client-side by markActiveNav() via data-nav +
|
||||||
aria-current; groups auto-expand when the current page is the category or
|
aria-current; groups auto-expand when the current page is the category or
|
||||||
one of its subcategories. #}
|
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">
|
|
||||||
|
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')) }}
|
{{ t(key="categories", lang=lang | default(value='sk')) }}
|
||||||
</p>
|
</p>
|
||||||
<ul class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-1">
|
||||||
<li>
|
<a href="/shop" data-nav="/shop"
|
||||||
<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">
|
||||||
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')) }}
|
||||||
{{ t(key="all-products", lang=lang | default(value='sk')) }}
|
</a>
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% for group in category_groups %}
|
{% for group in category_groups %}
|
||||||
{% if group.children | length > 0 %}
|
{% 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)">
|
x-init="open = ['{{ group.slug }}'{% for child in group.children %}, '{{ child.slug }}'{% endfor %}].some(s => location.pathname === '/category/' + s)">
|
||||||
<div class="flex items-stretch">
|
<div class="flex items-stretch">
|
||||||
<a href="/category/{{ group.slug }}" data-nav="/category/{{ group.slug }}"
|
<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 }}
|
{{ group.name }}
|
||||||
</a>
|
</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 }}"
|
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">
|
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" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
|
||||||
class="size-4 transition-transform" :class="open && 'rotate-90'">
|
class="size-5 shrink-0 transition-transform rotate-0" x-bind:class="open ? 'rotate-180' : 'rotate-0'" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 %}
|
{% for child in group.children %}
|
||||||
<li>
|
<li>
|
||||||
<a href="/category/{{ child.slug }}" data-nav="/category/{{ child.slug }}" style="padding-left: 28px"
|
<a href="/category/{{ child.slug }}" data-nav="/category/{{ child.slug }}"
|
||||||
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">
|
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">
|
||||||
<span class="text-on-surface/40 dark:text-on-surface-dark/40">↳</span>
|
{{ child.name }}
|
||||||
<span class="truncate">{{ child.name }}</span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>
|
<a href="/category/{{ group.slug }}" data-nav="/category/{{ group.slug }}"
|
||||||
<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">
|
||||||
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 }}
|
||||||
{{ group.name }}
|
</a>
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</div>
|
||||||
{% if category_groups | length == 0 %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ t(key="cart-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
{% block title %}{{ t(key="cart-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ category.name }}{% endblock title %}
|
{% block title %}{{ category.name }}{% endblock title %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ t(key="checkout-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
{% 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"
|
<form method="post" action="/checkout" hx-boost="false"
|
||||||
x-data="{
|
x-data="{
|
||||||
paymentMethod: '',
|
paymentMethod: '',
|
||||||
|
accountType: '{{ prefill_account_type | default(value='personal') }}',
|
||||||
carrier: '',
|
carrier: '',
|
||||||
carrierPrice: 0,
|
carrierPrice: 0,
|
||||||
requiresPoint: false,
|
requiresPoint: false,
|
||||||
@@ -30,25 +32,73 @@
|
|||||||
class="mt-6 grid gap-8 lg:grid-cols-3">
|
class="mt-6 grid gap-8 lg:grid-cols-3">
|
||||||
|
|
||||||
<div class="space-y-6 lg:col-span-2">
|
<div class="space-y-6 lg:col-span-2">
|
||||||
|
<!-- personal vs company. Fixed (read-only) for a logged-in account; a guest
|
||||||
|
picks it and the choice will type any account they create. -->
|
||||||
|
<fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-type", lang=lang | default(value='sk')) }}</legend>
|
||||||
|
{% if account_fixed %}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{% if prefill_account_type == "company" %}
|
||||||
|
{{ ui::badge(label=t(key="account-company", lang=lang | default(value='sk')), variant="primary") }}
|
||||||
|
{% else %}
|
||||||
|
{{ ui::badge(label=t(key="account-personal", lang=lang | default(value='sk')), variant="neutral") }}
|
||||||
|
{% endif %}
|
||||||
|
<span class="text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="account-type-locked", lang=lang | default(value='sk')) }}</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
||||||
|
{{ ui::radio(name="account_type", value="personal", attrs='x-model="accountType"') }}
|
||||||
|
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-personal", lang=lang | default(value='sk')) }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
||||||
|
{{ ui::radio(name="account_type", value="company", attrs='x-model="accountType"') }}
|
||||||
|
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company", lang=lang | default(value='sk')) }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- company billing details (company accounts only) -->
|
||||||
|
<fieldset x-show="accountType === 'company'" x-cloak class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="account-company-details", lang=lang | default(value='sk')) }}</legend>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="company_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-name", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
|
{{ ui::input(name="company_name", id="company_name", value=prefill_company_name | default(value=''), autocomplete="organization") }}
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-3">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="company_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-ico", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
|
{{ ui::input(name="company_id", id="company_id", value=prefill_company_id | default(value='')) }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="tax_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-dic", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
|
{{ ui::input(name="tax_id", id="tax_id", value=prefill_tax_id | default(value='')) }}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label for="vat_id" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="company-icdph", lang=lang | default(value='sk')) }} <span class="text-on-surface/50 dark:text-on-surface-dark/50">({{ t(key="field-optional", lang=lang | default(value='sk')) }})</span></label>
|
||||||
|
{{ ui::input(name="vat_id", id="vat_id", value=prefill_vat_id | default(value='')) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<!-- contact -->
|
<!-- contact -->
|
||||||
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend>
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-contact", lang=lang | default(value='sk')) }}</legend>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="email" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-email", lang=lang | default(value='sk')) }}</label>
|
<label for="email" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-email", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
<input id="email" name="email" type="email" required
|
{{ ui::input(name="email", id="email", type="email", value=prefill_email | default(value=''), required=true, autocomplete="email") }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="customer_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-name", lang=lang | default(value='sk')) }}</label>
|
<label for="customer_name" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-name", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
<input id="customer_name" name="customer_name" type="text" required
|
{{ ui::input(name="customer_name", id="customer_name", value=prefill_name | default(value=''), required=true, autocomplete="name") }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="phone" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-phone", lang=lang | default(value='sk')) }}</label>
|
<label for="phone" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-phone", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<!-- editable combobox: type freely or pick from the dropdown -->
|
<!-- editable combobox: type freely or pick from the dropdown -->
|
||||||
<div class="relative w-28 shrink-0" @click.outside="prefixOpen = false"
|
<div class="relative w-28 shrink-0" @click.outside="prefixOpen = false"
|
||||||
x-data="{ prefixOpen: false, prefix: '+421', opts: [
|
x-data="{ prefixOpen: false, prefix: '{{ prefill_phone_prefix | default(value='+421') }}', opts: [
|
||||||
{ v: '+421', l: '🇸🇰 +421' }, { v: '+420', l: '🇨🇿 +420' },
|
{ v: '+421', l: '🇸🇰 +421' }, { v: '+420', l: '🇨🇿 +420' },
|
||||||
{ v: '+43', l: '🇦🇹 +43' }, { v: '+49', l: '🇩🇪 +49' },
|
{ v: '+43', l: '🇦🇹 +43' }, { v: '+49', l: '🇩🇪 +49' },
|
||||||
{ v: '+48', l: '🇵🇱 +48' }, { v: '+36', l: '🇭🇺 +36' },
|
{ v: '+48', l: '🇵🇱 +48' }, { v: '+36', l: '🇭🇺 +36' },
|
||||||
@@ -72,8 +122,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<input id="phone" name="phone" type="tel" required autocomplete="tel" inputmode="tel" placeholder="900 000 000"
|
{{ ui::input(name="phone", id="phone", type="tel", value=prefill_phone | default(value=''), required=true, autocomplete="tel", placeholder="900 000 000", attrs='inputmode="tel"') }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -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">
|
<fieldset class="space-y-4 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</legend>
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-shipping", lang=lang | default(value='sk')) }}</legend>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="address" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-address", lang=lang | default(value='sk')) }}</label>
|
<label for="address" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-address", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
<input id="address" name="address" type="text" required
|
{{ ui::input(name="address", id="address", value=prefill_address | default(value=''), required=true, autocomplete="street-address") }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-4 sm:grid-cols-3">
|
<div class="grid gap-4 sm:grid-cols-3">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="city" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-city", lang=lang | default(value='sk')) }}</label>
|
<label for="city" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-city", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
<input id="city" name="city" type="text" required
|
{{ ui::input(name="city", id="city", value=prefill_city | default(value=''), required=true, autocomplete="address-level2") }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="zip" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-zip", lang=lang | default(value='sk')) }}</label>
|
<label for="zip" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-zip", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
<input id="zip" name="zip" type="text" required
|
{{ ui::input(name="zip", id="zip", value=prefill_zip | default(value=''), required=true, autocomplete="postal-code") }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="country" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-country", lang=lang | default(value='sk')) }}</label>
|
<label for="country" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-country", lang=lang | default(value='sk')) }}{{ ui::req() }}</label>
|
||||||
<div class="relative" @click.outside="countryOpen = false"
|
<div class="relative" @click.outside="countryOpen = false"
|
||||||
x-data="{ countryOpen: false, country: '{{ t(key='country-sk', lang=lang | default(value='sk')) }}', opts: [
|
x-data="{ countryOpen: false, country: '{{ prefill_country | default(value=t(key='country-sk', lang=lang | default(value='sk'))) }}', opts: [
|
||||||
{ v: '{{ t(key='country-sk', lang=lang | default(value='sk')) }}', l: '🇸🇰 {{ t(key='country-sk', lang=lang | default(value='sk')) }}' },
|
{ v: '{{ t(key='country-sk', lang=lang | default(value='sk')) }}', l: '🇸🇰 {{ t(key='country-sk', lang=lang | default(value='sk')) }}' },
|
||||||
{ v: '{{ t(key='country-cz', lang=lang | default(value='sk')) }}', l: '🇨🇿 {{ t(key='country-cz', lang=lang | default(value='sk')) }}' },
|
{ v: '{{ t(key='country-cz', lang=lang | default(value='sk')) }}', l: '🇨🇿 {{ t(key='country-cz', lang=lang | default(value='sk')) }}' },
|
||||||
{ v: '{{ t(key='country-at', lang=lang | default(value='sk')) }}', l: '🇦🇹 {{ t(key='country-at', lang=lang | default(value='sk')) }}' },
|
{ v: '{{ t(key='country-at', lang=lang | default(value='sk')) }}', l: '🇦🇹 {{ t(key='country-at', lang=lang | default(value='sk')) }}' },
|
||||||
@@ -131,13 +177,14 @@
|
|||||||
|
|
||||||
<!-- carrier -->
|
<!-- carrier -->
|
||||||
<fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
<fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-carrier", lang=lang | default(value='sk')) }}</legend>
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-carrier", lang=lang | default(value='sk')) }}{{ ui::req() }}</legend>
|
||||||
{% for m in shipping_methods %}
|
{% for m in shipping_methods %}
|
||||||
<label class="flex cursor-pointer items-center justify-between gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
<label class="flex cursor-pointer items-center justify-between gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
||||||
<span class="flex items-center gap-3">
|
<span class="flex items-center gap-3">
|
||||||
|
<!-- Penguin radio dot inline (the @change mixes nested single+double quotes, can't pass through a Tera macro arg) -->
|
||||||
<input type="radio" name="carrier_code" value="{{ m.code }}" required
|
<input type="radio" name="carrier_code" value="{{ m.code }}" required
|
||||||
@change="carrier='{{ m.code }}'; carrierPrice={{ m.price_cents }}; requiresPoint={{ m.requires_pickup_point }}; pointId=''; pointName=''"
|
@change="carrier='{{ m.code }}'; carrierPrice={{ m.price_cents }}; requiresPoint={{ m.requires_pickup_point }}; pointId=''; pointName=''"
|
||||||
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark">
|
class="before:content[''] relative h-4 w-4 appearance-none rounded-full border border-outline bg-surface before:invisible before:absolute before:left-1/2 before:top-1/2 before:h-1.5 before:w-1.5 before:-translate-x-1/2 before:-translate-y-1/2 before:rounded-full before:bg-on-primary checked:border-primary checked:bg-primary checked:before:visible focus:outline-2 focus:outline-offset-2 focus:outline-outline-strong checked:focus:outline-primary disabled:cursor-not-allowed dark:border-outline-dark dark:bg-surface-dark dark:before:bg-on-primary-dark dark:checked:border-primary-dark dark:checked:bg-primary-dark dark:focus:outline-outline-dark-strong dark:checked:focus:outline-primary-dark">
|
||||||
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ m.name }}</span>
|
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ m.name }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="tabular-nums text-on-surface/80 dark:text-on-surface-dark/80">{{ m.price }} {{ currency }}</span>
|
<span class="tabular-nums text-on-surface/80 dark:text-on-surface-dark/80">{{ m.price }} {{ currency }}</span>
|
||||||
@@ -166,24 +213,35 @@
|
|||||||
|
|
||||||
<!-- payment -->
|
<!-- payment -->
|
||||||
<fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
<fieldset class="space-y-3 rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-payment", lang=lang | default(value='sk')) }}</legend>
|
<legend class="px-1 text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-payment", lang=lang | default(value='sk')) }}{{ ui::req() }}</legend>
|
||||||
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
||||||
<input type="radio" name="payment_method" value="cod" required x-model="paymentMethod"
|
{{ ui::radio(name="payment_method", value="cod", attrs='required x-model="paymentMethod"') }}
|
||||||
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark">
|
|
||||||
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-cod", lang=lang | default(value='sk')) }}</span>
|
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-cod", lang=lang | default(value='sk')) }}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
<label class="flex cursor-pointer items-center gap-3 rounded-radius border border-outline px-4 py-3 transition has-[:checked]:border-primary dark:border-outline-dark dark:has-[:checked]:border-primary-dark">
|
||||||
<input type="radio" name="payment_method" value="bank_transfer" required x-model="paymentMethod"
|
{{ ui::radio(name="payment_method", value="bank_transfer", attrs='required x-model="paymentMethod"') }}
|
||||||
class="size-4 border-outline text-primary focus:ring-primary dark:border-outline-dark">
|
|
||||||
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-bank", lang=lang | default(value='sk')) }}</span>
|
<span class="font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="payment-bank", lang=lang | default(value='sk')) }}</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="note" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-note", lang=lang | default(value='sk')) }}</label>
|
<label for="note" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="checkout-note", lang=lang | default(value='sk')) }}</label>
|
||||||
<textarea id="note" name="note" rows="3"
|
{{ ui::textarea(name="note", id="note", rows="3") }}
|
||||||
class="w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark"></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if logged_in_customer and not profile_filled %}
|
||||||
|
<!-- offered only when the profile has no saved address yet; if it was filled
|
||||||
|
in advance we leave it untouched -->
|
||||||
|
{{ ui::checkbox(name="save_profile", id="save_profile", label=t(key="checkout-save-profile", lang=lang | default(value='sk'))) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if can_create_account %}
|
||||||
|
<!-- guests may turn this order into an account (typed by their choice above) -->
|
||||||
|
<div class="space-y-1.5 rounded-radius border border-outline bg-surface p-4 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
{{ ui::checkbox(name="create_account", id="create_account", label=t(key="checkout-create-account", lang=lang | default(value='sk'))) }}
|
||||||
|
<p class="pl-6 text-xs text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="checkout-create-account-hint", lang=lang | default(value='sk')) }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- summary -->
|
<!-- summary -->
|
||||||
@@ -211,10 +269,7 @@
|
|||||||
<span>{{ t(key="cart-total", lang=lang | default(value='sk')) }}</span>
|
<span>{{ t(key="cart-total", lang=lang | default(value='sk')) }}</span>
|
||||||
<span class="tabular-nums text-primary dark:text-primary-dark" x-text="fmt(subtotal + carrierPrice) + ' {{ currency }}'"></span>
|
<span class="tabular-nums text-primary dark:text-primary-dark" x-text="fmt(subtotal + carrierPrice) + ' {{ currency }}'"></span>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" :disabled="!canSubmit"
|
{{ 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") }}
|
||||||
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>
|
|
||||||
</aside>
|
</aside>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ t(key="nav-shop", lang=lang | default(value='sk')) }}{% endblock title %}
|
{% block title %}{{ t(key="nav-shop", lang=lang | default(value='sk')) }}{% endblock title %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ t(key="order-confirmed-title", lang=lang | default(value='sk')) }}{% endblock title %}
|
{% 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>
|
<p class="mt-1 text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-confirmed-sub", lang=lang | default(value='sk')) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if account_created %}
|
||||||
|
<div class="rounded-radius border border-primary/40 bg-primary/5 p-4 text-sm text-on-surface dark:border-primary-dark/40 dark:text-on-surface-dark" role="status">
|
||||||
|
{{ t(key="order-account-created", lang=lang | default(value='sk')) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
<div class="rounded-radius border border-outline bg-surface p-6 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
<div class="flex flex-wrap justify-between gap-2 border-b border-outline pb-3 dark:border-outline-dark">
|
<div class="flex flex-wrap justify-between gap-2 border-b border-outline pb-3 dark:border-outline-dark">
|
||||||
<span class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-number", lang=lang | default(value='sk')) }}</span>
|
<span class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="order-number", lang=lang | default(value='sk')) }}</span>
|
||||||
@@ -55,7 +62,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="text-center">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -1,17 +1,39 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% import "macros/ui.html" as ui %}
|
||||||
|
|
||||||
{% block title %}{{ product.name }}{% endblock title %}
|
{% block title %}{{ product.name }}{% endblock title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="grid gap-10 lg:grid-cols-2">
|
<div class="grid gap-10 lg:grid-cols-2">
|
||||||
<!-- gallery -->
|
<!-- gallery — prev/next arrows + opacity transitions adapted from
|
||||||
<div x-data="{ active: 0 }" class="space-y-4">
|
penguinui/carousel/default-carousel.html; kept our product thumbnail strip
|
||||||
<div class="aspect-square overflow-hidden rounded-radius border border-outline bg-surface-alt dark:border-outline-dark dark:bg-surface-dark-alt">
|
(more useful than carousel dots for a product) and 0-based `active` -->
|
||||||
|
<div x-data="{ active: 0, count: {{ images | length }},
|
||||||
|
prev() { this.active = this.active > 0 ? this.active - 1 : this.count - 1 },
|
||||||
|
next() { this.active = this.active < this.count - 1 ? this.active + 1 : 0 } }"
|
||||||
|
class="space-y-4">
|
||||||
|
<div class="relative aspect-square overflow-hidden rounded-radius border border-outline bg-surface-alt dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
{% if images | length > 0 %}
|
{% if images | length > 0 %}
|
||||||
{% for image in images %}
|
{% for image in images %}
|
||||||
<img x-show="active === {{ loop.index0 }}" src="/images/{{ image }}" alt="{{ product.name }}" class="size-full object-cover">
|
<img x-show="active === {{ loop.index0 }}" x-transition.opacity.duration.300ms src="/images/{{ image }}" alt="{{ product.name }}" class="absolute inset-0 size-full object-cover">
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if images | length > 1 %}
|
||||||
|
<!-- previous slide -->
|
||||||
|
<button type="button" @click="prev()" aria-label="{{ t(key='gallery-prev', lang=lang | default(value='sk')) }}"
|
||||||
|
class="absolute left-3 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full bg-surface/40 p-2 text-on-surface transition hover:bg-surface/60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:bg-surface-dark/40 dark:text-on-surface-dark dark:hover:bg-surface-dark/60 dark:focus-visible:outline-primary-dark">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="3" class="size-5" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<!-- next slide -->
|
||||||
|
<button type="button" @click="next()" aria-label="{{ t(key='gallery-next', lang=lang | default(value='sk')) }}"
|
||||||
|
class="absolute right-3 top-1/2 z-10 flex -translate-y-1/2 items-center justify-center rounded-full bg-surface/40 p-2 text-on-surface transition hover:bg-surface/60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:bg-surface-dark/40 dark:text-on-surface-dark dark:hover:bg-surface-dark/60 dark:focus-visible:outline-primary-dark">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="3" class="size-5" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if images | length > 1 %}
|
{% if images | length > 1 %}
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
@@ -39,17 +61,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if product.stock > 0 %}
|
{% 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 }}">
|
<input type="hidden" name="product_id" value="{{ product.id }}">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label for="quantity" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="quantity", lang=lang | default(value='sk')) }}</label>
|
<label for="quantity" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="quantity", lang=lang | default(value='sk')) }}</label>
|
||||||
<input id="quantity" name="quantity" type="number" min="1" max="{{ product.stock }}" value="1"
|
{{ ui::input(name="quantity", id="quantity", type="number", value="1", width="w-24", attrs='min="1" max="' ~ product.stock ~ '"') }}
|
||||||
class="w-24 rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface focus:outline-2 focus:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit"
|
{{ ui::button(label=t(key="add-to-cart", lang=lang | default(value='sk')), type="submit", size="px-5 py-2 text-sm") }}
|
||||||
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>
|
|
||||||
</form>
|
</form>
|
||||||
<p class="text-sm text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="in-stock", lang=lang | default(value='sk')) }}: {{ product.stock }}</p>
|
<p class="text-sm text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="in-stock", lang=lang | default(value='sk')) }}: {{ product.stock }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
24
config/casbin/model.conf
Normal file
24
config/casbin/model.conf
Normal 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
25
config/casbin/policy.csv
Normal 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.
|
@@ -45,19 +45,24 @@ workers:
|
|||||||
|
|
||||||
# Mailer Configuration.
|
# Mailer Configuration.
|
||||||
mailer:
|
mailer:
|
||||||
# SMTP mailer configuration.
|
# SMTP mailer configuration. Defaults target a local catcher (MailHog/Mailpit
|
||||||
|
# on localhost:1025); set the SMTP_* env vars to point at a real server. The
|
||||||
|
# auth block is only emitted when SMTP_PASSWORD is provided, so the secret is
|
||||||
|
# never stored here — pass it in at launch (e.g. from `pass`).
|
||||||
smtp:
|
smtp:
|
||||||
# Enable/Disable smtp mailer.
|
# Enable/Disable smtp mailer.
|
||||||
enable: true
|
enable: {{ get_env(name="SMTP_ENABLE", default="true") }}
|
||||||
# SMTP server host. e.x localhost, smtp.gmail.com
|
# SMTP server host. e.x localhost, smtp.gmail.com
|
||||||
host: localhost
|
host: "{{ get_env(name="SMTP_HOST", default="localhost") }}"
|
||||||
# SMTP server port
|
# SMTP server port
|
||||||
port: 1025
|
port: {{ get_env(name="SMTP_PORT", default="1025") }}
|
||||||
# Use secure connection (SSL/TLS).
|
# Use secure connection (SSL/TLS).
|
||||||
secure: false
|
secure: {{ get_env(name="SMTP_SECURE", default="false") }}
|
||||||
# auth:
|
{% if get_env(name="SMTP_PASSWORD", default="") != "" %}
|
||||||
# user:
|
auth:
|
||||||
# password:
|
user: "{{ get_env(name="SMTP_USER", default="") }}"
|
||||||
|
password: "{{ get_env(name="SMTP_PASSWORD", default="") }}"
|
||||||
|
{% endif %}
|
||||||
# Override the SMTP hello name (default is the machine's hostname)
|
# Override the SMTP hello name (default is the machine's hostname)
|
||||||
# hello_name:
|
# hello_name:
|
||||||
|
|
||||||
@@ -125,3 +130,30 @@ settings:
|
|||||||
# Bank-transfer payment details shown on the order confirmation.
|
# Bank-transfer payment details shown on the order confirmation.
|
||||||
bank_iban: {{ get_env(name="BANK_IBAN", default="SK00 0000 0000 0000 0000 0000") }}
|
bank_iban: {{ get_env(name="BANK_IBAN", default="SK00 0000 0000 0000 0000 0000") }}
|
||||||
bank_account_name: {{ get_env(name="BANK_ACCOUNT_NAME", default="Kompress s.r.o.") }}
|
bank_account_name: {{ get_env(name="BANK_ACCOUNT_NAME", default="Kompress s.r.o.") }}
|
||||||
|
|
||||||
|
# loco-oauth2: social login. Credentials come from .env (create them in the
|
||||||
|
# Google Cloud console and register the redirect_url below as an authorized
|
||||||
|
# redirect URI). Until OAUTH_CLIENT_ID/SECRET are set, the "Continue with
|
||||||
|
# Google" button will fail at the consent screen — the rest of auth is unaffected.
|
||||||
|
initializers:
|
||||||
|
oauth2:
|
||||||
|
# Key for the loco-oauth2 private cookie jar (>= 64 bytes). Override in prod.
|
||||||
|
secret_key: {{ get_env(name="OAUTH_PRIVATE_KEY", default="144, 76, 183, 1, 15, 184, 233, 174, 214, 251, 190, 186, 122, 61, 74, 84, 225, 110, 189, 115, 10, 251, 133, 128, 52, 46, 15, 66, 85, 1, 245, 73, 27, 113, 189, 15, 209, 205, 61, 100, 73, 31, 18, 58, 235, 105, 141, 36, 70, 92, 231, 151, 27, 32, 243, 117, 30, 244, 110, 89, 233, 196, 137, 130") }}
|
||||||
|
authorization_code:
|
||||||
|
- client_identifier: google
|
||||||
|
client_credentials:
|
||||||
|
client_id: {{ get_env(name="OAUTH_CLIENT_ID", default="oauth_client_id") }}
|
||||||
|
client_secret: {{ get_env(name="OAUTH_CLIENT_SECRET", default="oauth_client_secret") }}
|
||||||
|
url_config:
|
||||||
|
auth_url: {{ get_env(name="OAUTH_AUTH_URL", default="https://accounts.google.com/o/oauth2/auth") }}
|
||||||
|
token_url: {{ get_env(name="OAUTH_TOKEN_URL", default="https://www.googleapis.com/oauth2/v3/token") }}
|
||||||
|
redirect_url: {{ get_env(name="OAUTH_REDIRECT_URL", default="http://localhost:5150/api/oauth2/google/callback/cookie") }}
|
||||||
|
profile_url: {{ get_env(name="OAUTH_PROFILE_URL", default="https://openidconnect.googleapis.com/v1/userinfo") }}
|
||||||
|
scopes:
|
||||||
|
- "https://www.googleapis.com/auth/userinfo.email"
|
||||||
|
- "https://www.googleapis.com/auth/userinfo.profile"
|
||||||
|
cookie_config:
|
||||||
|
# After loco-oauth2 sets its session cookie it redirects here, where we
|
||||||
|
# mint our own auth_token cookie (see controllers/oauth2.rs::complete).
|
||||||
|
protected_url: {{ get_env(name="OAUTH_PROTECTED_URL", default="http://localhost:5150/api/oauth2/protected") }}
|
||||||
|
timeout_seconds: 600
|
||||||
|
|||||||
@@ -55,3 +55,25 @@ auth:
|
|||||||
settings:
|
settings:
|
||||||
admin_email: "{{ get_env(name="ADMIN_EMAIL", default="") }}"
|
admin_email: "{{ get_env(name="ADMIN_EMAIL", default="") }}"
|
||||||
uploads_root: "{{ get_env(name="UPLOADS_ROOT", default="data/uploads") }}"
|
uploads_root: "{{ get_env(name="UPLOADS_ROOT", default="data/uploads") }}"
|
||||||
|
|
||||||
|
# loco-oauth2 social login. All values must come from the environment in prod;
|
||||||
|
# OAUTH_REDIRECT_URL / OAUTH_PROTECTED_URL must use the real public origin.
|
||||||
|
initializers:
|
||||||
|
oauth2:
|
||||||
|
secret_key: "{{ get_env(name="OAUTH_PRIVATE_KEY") }}"
|
||||||
|
authorization_code:
|
||||||
|
- client_identifier: google
|
||||||
|
client_credentials:
|
||||||
|
client_id: "{{ get_env(name="OAUTH_CLIENT_ID") }}"
|
||||||
|
client_secret: "{{ get_env(name="OAUTH_CLIENT_SECRET") }}"
|
||||||
|
url_config:
|
||||||
|
auth_url: "{{ get_env(name="OAUTH_AUTH_URL", default="https://accounts.google.com/o/oauth2/auth") }}"
|
||||||
|
token_url: "{{ get_env(name="OAUTH_TOKEN_URL", default="https://www.googleapis.com/oauth2/v3/token") }}"
|
||||||
|
redirect_url: "{{ get_env(name="OAUTH_REDIRECT_URL") }}"
|
||||||
|
profile_url: "{{ get_env(name="OAUTH_PROFILE_URL", default="https://openidconnect.googleapis.com/v1/userinfo") }}"
|
||||||
|
scopes:
|
||||||
|
- "https://www.googleapis.com/auth/userinfo.email"
|
||||||
|
- "https://www.googleapis.com/auth/userinfo.profile"
|
||||||
|
cookie_config:
|
||||||
|
protected_url: "{{ get_env(name="OAUTH_PROTECTED_URL") }}"
|
||||||
|
timeout_seconds: 600
|
||||||
|
|||||||
@@ -42,33 +42,58 @@ workers:
|
|||||||
|
|
||||||
|
|
||||||
# Mailer Configuration.
|
# Mailer Configuration.
|
||||||
|
# Defaults keep the whole suite on the in-memory stub mailer. The real-SMTP
|
||||||
|
# smoke test (tests/mailer/smtp_send.rs) opts in by setting these env vars
|
||||||
|
# before boot; nothing else in the suite sends real mail.
|
||||||
mailer:
|
mailer:
|
||||||
stub: true
|
stub: {{ get_env(name="MAILER_STUB", default="true") }}
|
||||||
# SMTP mailer configuration.
|
# SMTP mailer configuration.
|
||||||
smtp:
|
smtp:
|
||||||
# Enable/Disable smtp mailer.
|
# Enable/Disable smtp mailer.
|
||||||
enable: true
|
enable: {{ get_env(name="SMTP_ENABLE", default="true") }}
|
||||||
# SMTP server host. e.x localhost, smtp.gmail.com
|
# SMTP server host. e.x localhost, smtp.gmail.com
|
||||||
host: localhost
|
host: "{{ get_env(name="SMTP_HOST", default="localhost") }}"
|
||||||
# SMTP server port
|
# SMTP server port
|
||||||
port: 1025
|
port: {{ get_env(name="SMTP_PORT", default="1025") }}
|
||||||
# Use secure connection (SSL/TLS).
|
# Use secure connection (SSL/TLS).
|
||||||
secure: false
|
secure: {{ get_env(name="SMTP_SECURE", default="false") }}
|
||||||
# auth:
|
auth:
|
||||||
# user:
|
user: "{{ get_env(name="SMTP_USER", default="") }}"
|
||||||
# password:
|
password: "{{ get_env(name="SMTP_PASSWORD", default="") }}"
|
||||||
|
|
||||||
# Initializers Configuration
|
# Initializers Configuration
|
||||||
# initializers:
|
# OAuth2StoreInitializer requires this block to boot (it builds the client store
|
||||||
# oauth2:
|
# in after_routes). Static, non-secret placeholders: tests never perform a real
|
||||||
# authorization_code: # Authorization code grant type
|
# OAuth2 handshake, they just need the store to construct successfully.
|
||||||
# - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config.
|
initializers:
|
||||||
# ... other fields
|
oauth2:
|
||||||
|
# Private-cookie key: a ", "-separated list of >=64 byte values (not a
|
||||||
|
# plain string). This is loco-oauth2's documented sample key; fine for tests.
|
||||||
|
secret_key: "144, 76, 183, 1, 15, 184, 233, 174, 214, 251, 190, 186, 122, 61, 74, 84, 225, 110, 189, 115, 10, 251, 133, 128, 52, 46, 15, 66, 85, 1, 245, 73, 27, 113, 189, 15, 209, 205, 61, 100, 73, 31, 18, 58, 235, 105, 141, 36, 70, 92, 231, 151, 27, 32, 243, 117, 30, 244, 110, 89, 233, 196, 137, 130"
|
||||||
|
authorization_code:
|
||||||
|
- client_identifier: google
|
||||||
|
client_credentials:
|
||||||
|
client_id: test-client-id
|
||||||
|
client_secret: test-client-secret
|
||||||
|
url_config:
|
||||||
|
auth_url: https://accounts.google.com/o/oauth2/auth
|
||||||
|
token_url: https://www.googleapis.com/oauth2/v3/token
|
||||||
|
redirect_url: http://localhost:5150/api/oauth2/google/callback
|
||||||
|
profile_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||||
|
scopes:
|
||||||
|
- https://www.googleapis.com/auth/userinfo.email
|
||||||
|
- https://www.googleapis.com/auth/userinfo.profile
|
||||||
|
cookie_config:
|
||||||
|
protected_url: http://localhost:5150/
|
||||||
|
timeout_seconds: 600
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
database:
|
database:
|
||||||
# Database connection URI
|
# Database connection URI. Pinned to the throwaway test DB and intentionally
|
||||||
uri: {{ get_env(name="DATABASE_URL", default="postgres://uni_loco_web_user:3@localhost:5432/kompress_eshop_test") }}
|
# NOT read from `DATABASE_URL`: the app loads `.env` on boot (app.rs
|
||||||
|
# `load_config`), and this config has `dangerously_recreate: true`, so honoring
|
||||||
|
# an env override here would let `cargo test` recreate the dev/prod database.
|
||||||
|
uri: "postgres://uni_loco_web_user:3@localhost:5432/kompress_eshop_test"
|
||||||
# When enabled, the sql query will be logged.
|
# When enabled, the sql query will be logged.
|
||||||
enable_logging: false
|
enable_logging: false
|
||||||
# Set the timeout duration when acquiring a connection.
|
# Set the timeout duration when acquiring a connection.
|
||||||
|
|||||||
104
docs/integrations/google-oauth.md
Normal file
104
docs/integrations/google-oauth.md
Normal 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`
|
||||||
@@ -86,7 +86,10 @@
|
|||||||
buildInputs = [
|
buildInputs = [
|
||||||
rust
|
rust
|
||||||
pkgs.pkg-config
|
pkgs.pkg-config
|
||||||
|
# OpenSSL for crypto dependencies (loco-oauth2 -> oauth2/reqwest
|
||||||
|
# use native-tls); .dev provides headers + pkg-config metadata.
|
||||||
pkgs.openssl
|
pkgs.openssl
|
||||||
|
pkgs.openssl.dev
|
||||||
pkgs.cmake
|
pkgs.cmake
|
||||||
pkgs.llvmPackages.clang
|
pkgs.llvmPackages.clang
|
||||||
pkgs.llvmPackages.libclang.lib
|
pkgs.llvmPackages.libclang.lib
|
||||||
|
|||||||
637
hardcoded-inventory.md
Normal file
637
hardcoded-inventory.md
Normal 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 (#18–27) 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 (#18–27) are already internally Penguin-adapted or have
|
||||||
|
no applicable match — the migration is effectively complete.**
|
||||||
@@ -13,7 +13,7 @@ loco-rs = { workspace = true }
|
|||||||
|
|
||||||
|
|
||||||
[dependencies.sea-orm-migration]
|
[dependencies.sea-orm-migration]
|
||||||
version = "1.1.0"
|
version = "1.1.20"
|
||||||
features = [
|
features = [
|
||||||
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
|
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
|
||||||
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
|
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ mod m20260616_160000_add_parent_to_categories;
|
|||||||
mod m20260617_000001_add_carrier_to_shipping_methods;
|
mod m20260617_000001_add_carrier_to_shipping_methods;
|
||||||
mod m20260617_000002_add_shipment_to_orders;
|
mod m20260617_000002_add_shipment_to_orders;
|
||||||
mod m20260617_000003_add_phone_to_orders;
|
mod m20260617_000003_add_phone_to_orders;
|
||||||
|
mod m20260618_000001_o_auth2_sessions;
|
||||||
|
mod m20260618_000002_customer_profiles;
|
||||||
|
mod m20260618_000003_account_type;
|
||||||
|
mod m20260618_000004_account_ownership;
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -64,6 +68,10 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260617_000001_add_carrier_to_shipping_methods::Migration),
|
Box::new(m20260617_000001_add_carrier_to_shipping_methods::Migration),
|
||||||
Box::new(m20260617_000002_add_shipment_to_orders::Migration),
|
Box::new(m20260617_000002_add_shipment_to_orders::Migration),
|
||||||
Box::new(m20260617_000003_add_phone_to_orders::Migration),
|
Box::new(m20260617_000003_add_phone_to_orders::Migration),
|
||||||
|
Box::new(m20260618_000001_o_auth2_sessions::Migration),
|
||||||
|
Box::new(m20260618_000002_customer_profiles::Migration),
|
||||||
|
Box::new(m20260618_000003_account_type::Migration),
|
||||||
|
Box::new(m20260618_000004_account_ownership::Migration),
|
||||||
// inject-above (do not remove this comment)
|
// inject-above (do not remove this comment)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
29
migration/src/m20260618_000001_o_auth2_sessions.rs
Normal file
29
migration/src/m20260618_000001_o_auth2_sessions.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
44
migration/src/m20260618_000002_customer_profiles.rs
Normal file
44
migration/src/m20260618_000002_customer_profiles.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
39
migration/src/m20260618_000003_account_type.rs
Normal file
39
migration/src/m20260618_000003_account_type.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
38
migration/src/m20260618_000004_account_ownership.rs
Normal file
38
migration/src/m20260618_000004_account_ownership.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
30
penguinui-components/README.md
Normal file
30
penguinui-components/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 🐧 Penguin UI
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
*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**
|
||||||
|
|
||||||
|
[](https://x.com/salar_houshvand)
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -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>
|
||||||
70
penguinui-components/accordion/default-accordion.html
Normal file
70
penguinui-components/accordion/default-accordion.html
Normal 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>
|
||||||
70
penguinui-components/accordion/single.open-accordion.html
Normal file
70
penguinui-components/accordion/single.open-accordion.html
Normal 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>
|
||||||
72
penguinui-components/accordion/split-accordion.html
Normal file
72
penguinui-components/accordion/split-accordion.html
Normal 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>
|
||||||
59
penguinui-components/ai-options/image-style-selector.html
Normal file
59
penguinui-components/ai-options/image-style-selector.html
Normal 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>
|
||||||
39
penguinui-components/ai-options/model-selector.html
Normal file
39
penguinui-components/ai-options/model-selector.html
Normal 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>
|
||||||
45
penguinui-components/ai-options/voice-tone-selector.html
Normal file
45
penguinui-components/ai-options/voice-tone-selector.html
Normal 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>
|
||||||
65
penguinui-components/ai-output-display/chat-display.html
Normal file
65
penguinui-components/ai-output-display/chat-display.html
Normal 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>
|
||||||
@@ -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>
|
||||||
82
penguinui-components/ai-output-display/image-output.html
Normal file
82
penguinui-components/ai-output-display/image-output.html
Normal 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>
|
||||||
@@ -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">
|
||||||
|
<<span class="text-pink-400">script</span>>
|
||||||
|
<span class="text-pink-400">console</span>.log("<span class="text-blue-400">Hello World!</span>")
|
||||||
|
</<span class="text-pink-400">script</span>>
|
||||||
|
</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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
122
penguinui-components/ai-output-interactions/share-widget.html
Normal file
122
penguinui-components/ai-output-interactions/share-widget.html
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
36
penguinui-components/ai-prompt-input/with-action-button.html
Normal file
36
penguinui-components/ai-prompt-input/with-action-button.html
Normal 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>
|
||||||
19
penguinui-components/ai-prompt-input/with-suggestions.html
Normal file
19
penguinui-components/ai-prompt-input/with-suggestions.html
Normal 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>
|
||||||
74
penguinui-components/ai-triggers/button.html
Normal file
74
penguinui-components/ai-triggers/button.html
Normal 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>
|
||||||
|
|
||||||
48
penguinui-components/ai-triggers/gradient-button.html
Normal file
48
penguinui-components/ai-triggers/gradient-button.html
Normal 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>
|
||||||
|
|
||||||
10
penguinui-components/ai-triggers/keyboard-shortcut.html
Normal file
10
penguinui-components/ai-triggers/keyboard-shortcut.html
Normal 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>
|
||||||
44
penguinui-components/ai-triggers/trigger-orb.html
Normal file
44
penguinui-components/ai-triggers/trigger-orb.html
Normal 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>
|
||||||
109
penguinui-components/alert/alert-dismiss-functionality.html
Normal file
109
penguinui-components/alert/alert-dismiss-functionality.html
Normal 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>
|
||||||
127
penguinui-components/alert/alert-with-action.html
Normal file
127
penguinui-components/alert/alert-with-action.html
Normal 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>
|
||||||
103
penguinui-components/alert/alert-with-link.html
Normal file
103
penguinui-components/alert/alert-with-link.html
Normal 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>
|
||||||
48
penguinui-components/alert/alert-with-list.html
Normal file
48
penguinui-components/alert/alert-with-list.html
Normal 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>
|
||||||
101
penguinui-components/alert/default-alert.html
Normal file
101
penguinui-components/alert/default-alert.html
Normal 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>
|
||||||
15
penguinui-components/avatar/avatar-with-border.html
Normal file
15
penguinui-components/avatar/avatar-with-border.html
Normal 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">
|
||||||
@@ -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>
|
||||||
|
|
||||||
24
penguinui-components/avatar/avatar-with-initials.html
Normal file
24
penguinui-components/avatar/avatar-with-initials.html
Normal 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>
|
||||||
|
|
||||||
35
penguinui-components/avatar/avatar-with-status.html
Normal file
35
penguinui-components/avatar/avatar-with-status.html
Normal 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>
|
||||||
|
|
||||||
1
penguinui-components/avatar/default-avatar.html
Normal file
1
penguinui-components/avatar/default-avatar.html
Normal 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">
|
||||||
1
penguinui-components/avatar/square-avatar.html
Normal file
1
penguinui-components/avatar/square-avatar.html
Normal 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">
|
||||||
6
penguinui-components/avatar/stacked-avatars.html
Normal file
6
penguinui-components/avatar/stacked-avatars.html
Normal 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>
|
||||||
36
penguinui-components/badge/animating-notification-badge.html
Normal file
36
penguinui-components/badge/animating-notification-badge.html
Normal 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>
|
||||||
|
|
||||||
60
penguinui-components/badge/badge-with-icon.html
Normal file
60
penguinui-components/badge/badge-with-icon.html
Normal 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>
|
||||||
|
|
||||||
64
penguinui-components/badge/badge-with-indicator.html
Normal file
64
penguinui-components/badge/badge-with-indicator.html
Normal 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>
|
||||||
|
|
||||||
24
penguinui-components/badge/default-badge.html
Normal file
24
penguinui-components/badge/default-badge.html
Normal 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>
|
||||||
|
|
||||||
14
penguinui-components/badge/notification-badge.html
Normal file
14
penguinui-components/badge/notification-badge.html
Normal 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>
|
||||||
40
penguinui-components/badge/soft-color-badge.html
Normal file
40
penguinui-components/badge/soft-color-badge.html
Normal 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>
|
||||||
|
|
||||||
11
penguinui-components/banner/banner-with-button.html
Normal file
11
penguinui-components/banner/banner-with-button.html
Normal 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>
|
||||||
22
penguinui-components/banner/cookie-banner.html
Normal file
22
penguinui-components/banner/cookie-banner.html
Normal 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>
|
||||||
8
penguinui-components/banner/fixed-banner.html
Normal file
8
penguinui-components/banner/fixed-banner.html
Normal 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>
|
||||||
8
penguinui-components/banner/simple-banner.html
Normal file
8
penguinui-components/banner/simple-banner.html
Normal 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>
|
||||||
@@ -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>
|
||||||
21
penguinui-components/breadcrumbs/breadcrumb-with-icon.html
Normal file
21
penguinui-components/breadcrumbs/breadcrumb-with-icon.html
Normal 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>
|
||||||
13
penguinui-components/breadcrumbs/breadcrumb-with-slash.html
Normal file
13
penguinui-components/breadcrumbs/breadcrumb-with-slash.html
Normal 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>
|
||||||
64
penguinui-components/buttons/button-with-icon.html
Normal file
64
penguinui-components/buttons/button-with-icon.html
Normal 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>
|
||||||
|
|
||||||
24
penguinui-components/buttons/default-button.html
Normal file
24
penguinui-components/buttons/default-button.html
Normal 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>
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user