mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
update compose window
This commit is contained in:
1 parent
70dc5c998c
commit
27f1f6bdb4
6 files changed
+184
-77
No files matched your search
+112
-43
@@ -1,10 +1,8 @@
|
||||
/*
|
||||
* tom-select.css
|
||||
* Overrides the UX Autocomplete / Tom Select default skin to match
|
||||
* the plMail compose window aesthetic.
|
||||
*
|
||||
* Loaded via @import in app.css. Set tom-select.default.css autoimport
|
||||
* to false in controllers.json so this is the only skin.
|
||||
* Custom skin for Symfony UX Autocomplete / Tom Select used in the
|
||||
* plMail compose window. All default Tom Select CSS bundles are disabled
|
||||
* in controllers.json — this is the sole skin.
|
||||
*/
|
||||
|
||||
/* ── Base reset ──────────────────────────────────────────────────────── */
|
||||
@@ -24,12 +22,18 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* UX Autocomplete wraps the <select> in an extra div — flatten it */
|
||||
.ts-wrapper.multi.has-items .ts-control,
|
||||
.ts-wrapper.multi .ts-control {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
/* ── Control (the visible input area) ───────────────────────────────── */
|
||||
.ts-control {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
gap: 4px;
|
||||
padding: 2px 0;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
@@ -40,7 +44,7 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* The actual text input Tom Select injects */
|
||||
/* The text input Tom Select injects */
|
||||
.ts-control input {
|
||||
flex: 1 1 80px;
|
||||
min-width: 80px;
|
||||
@@ -67,7 +71,7 @@
|
||||
color: rgb(107 114 128); /* gray-500 */
|
||||
}
|
||||
|
||||
/* Hide the default dropdown caret Tom Select appends */
|
||||
/* Hide the dropdown caret Tom Select appends to the wrapper */
|
||||
.ts-wrapper .ts-control::after,
|
||||
.ts-wrapper.single .ts-control::after,
|
||||
.ts-wrapper.multi .ts-control::after {
|
||||
@@ -78,8 +82,8 @@
|
||||
.ts-control .item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 1px 4px 1px 8px;
|
||||
gap: 4px;
|
||||
padding: 2px 6px 2px 9px;
|
||||
border-radius: 9999px;
|
||||
background-color: rgb(219 234 254); /* blue-100 */
|
||||
color: rgb(29 78 216); /* blue-700 */
|
||||
@@ -99,7 +103,7 @@
|
||||
color: rgb(147 197 253); /* blue-300 */
|
||||
}
|
||||
|
||||
/* × remove button */
|
||||
/* × remove button — per chip */
|
||||
.ts-control .item .remove {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -107,17 +111,17 @@
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 9999px;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
opacity: 0.55;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin-left: 1px;
|
||||
flex-shrink: 0;
|
||||
text-decoration: none;
|
||||
transition: opacity 100ms;
|
||||
}
|
||||
|
||||
@@ -125,12 +129,18 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Hide Tom Select's global clear button — we use per-chip removes */
|
||||
.ts-wrapper .clear-button,
|
||||
.ts-wrapper > .clear-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ── Dropdown panel ──────────────────────────────────────────────────── */
|
||||
.ts-dropdown {
|
||||
position: absolute;
|
||||
left: -4rem;
|
||||
right: -1rem;
|
||||
top: calc(100% + 2px);
|
||||
top: calc(100% + 4px);
|
||||
z-index: 40;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
@@ -165,31 +175,38 @@
|
||||
padding: 7px 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 80ms;
|
||||
color: rgb(55 65 81);
|
||||
color: rgb(55 65 81); /* gray-700 */
|
||||
}
|
||||
|
||||
.dark .ts-dropdown .option {
|
||||
color: rgb(229 231 235);
|
||||
color: rgb(229 231 235); /* gray-200 */
|
||||
}
|
||||
|
||||
.ts-dropdown .option.active,
|
||||
.ts-dropdown .option:hover {
|
||||
background-color: rgb(239 246 255);
|
||||
color: rgb(29 78 216);
|
||||
background-color: rgb(239 246 255); /* blue-50 */
|
||||
color: rgb(29 78 216); /* blue-700 */
|
||||
}
|
||||
|
||||
.dark .ts-dropdown .option.active,
|
||||
.dark .ts-dropdown .option:hover {
|
||||
background-color: rgba(37 99 235 / 0.15);
|
||||
color: rgb(147 197 253);
|
||||
color: rgb(147 197 253); /* blue-300 */
|
||||
}
|
||||
|
||||
.ts-dropdown .option.selected {
|
||||
background-color: transparent;
|
||||
opacity: 0.5;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* ── Option inner layout ─────────────────────────────────────────────── */
|
||||
.ts-option-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
/* ── Option inner layout: avatar + text block ────────────────────────── */
|
||||
.ts-option-avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -232,50 +249,72 @@
|
||||
}
|
||||
|
||||
/* ── "Add …" create row ──────────────────────────────────────────────── */
|
||||
.ts-dropdown .create {
|
||||
.ts-option-create {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 14px;
|
||||
font-size: 0.8125rem;
|
||||
color: rgb(107 114 128);
|
||||
color: rgb(107 114 128); /* gray-500 */
|
||||
}
|
||||
|
||||
.dark .ts-option-create {
|
||||
color: rgb(156 163 175); /* gray-400 */
|
||||
}
|
||||
|
||||
.ts-option-create-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9999px;
|
||||
background-color: rgb(219 234 254); /* blue-100 */
|
||||
color: rgb(37 99 235); /* blue-600 */
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dark .ts-option-create-icon {
|
||||
background-color: rgba(37 99 235 / 0.2);
|
||||
color: rgb(147 197 253);
|
||||
}
|
||||
|
||||
.ts-dropdown .create {
|
||||
padding: 7px 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 80ms;
|
||||
border-top: 1px solid rgba(209 213 219 / 0.5);
|
||||
}
|
||||
|
||||
.dark .ts-dropdown .create {
|
||||
color: rgb(156 163 175);
|
||||
border-top-color: rgba(255 255 255 / 0.08);
|
||||
}
|
||||
|
||||
.ts-dropdown .create:hover,
|
||||
.ts-dropdown .create.active {
|
||||
background-color: rgb(249 250 251);
|
||||
color: rgb(31 41 55);
|
||||
}
|
||||
|
||||
.dark .ts-dropdown .create:hover,
|
||||
.dark .ts-dropdown .create.active {
|
||||
background-color: rgba(255 255 255 / 0.05);
|
||||
color: rgb(229 231 235);
|
||||
}
|
||||
|
||||
/* ── No results ──────────────────────────────────────────────────────── */
|
||||
.ts-dropdown .no-results,
|
||||
.ts-dropdown .no_results {
|
||||
.ts-no-results {
|
||||
padding: 10px 14px;
|
||||
font-size: 0.8125rem;
|
||||
color: rgb(156 163 175);
|
||||
color: rgb(156 163 175); /* gray-400 */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Spinner ─────────────────────────────────────────────────────────── */
|
||||
/* ── Spinner — hidden, UX Autocomplete handles loading state ─────────── */
|
||||
.ts-wrapper .spinner {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ── Focus / open state ──────────────────────────────────────────────── */
|
||||
/* ── Focus / open state — keep frameless ─────────────────────────────── */
|
||||
.ts-wrapper.focus .ts-control,
|
||||
.ts-wrapper.dropdown-active .ts-control {
|
||||
outline: none !important;
|
||||
@@ -290,19 +329,49 @@
|
||||
}
|
||||
|
||||
/* ── Thin scrollbar ──────────────────────────────────────────────────── */
|
||||
.ts-dropdown-content::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.ts-dropdown-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ts-dropdown-content::-webkit-scrollbar { width: 4px; }
|
||||
.ts-dropdown-content::-webkit-scrollbar-track { background: transparent; }
|
||||
.ts-dropdown-content::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156 163 175 / 0.45);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.dark .ts-dropdown-content::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(75 85 99 / 0.5);
|
||||
}
|
||||
/* Hide the original <select> that Tom Select moves off-screen */
|
||||
.ts-hidden-accessible {
|
||||
position: absolute !important;
|
||||
left: -10000px !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0,0,0,0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
/* remove_button plugin — × button appended to each chip by Tom Select */
|
||||
.ts-control .item .remove {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 9999px;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin-left: 2px;
|
||||
flex-shrink: 0;
|
||||
text-decoration: none;
|
||||
transition: opacity 100ms;
|
||||
}
|
||||
|
||||
.ts-control .item .remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -1508,8 +1508,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* }
|
||||
* @psalm-type MercureConfig = array{
|
||||
* hubs?: array<string, array{ // Default: []
|
||||
* url?: scalar|Param|null, // URL of the hub's publish endpoint // Default: null
|
||||
* public_url?: scalar|Param|null, // URL of the hub's public endpoint
|
||||
* url?: scalar|Param|null, // URL of the hub's publish endpoint
|
||||
* public_url?: scalar|Param|null, // URL of the hub's public endpoint // Default: null
|
||||
* jwt?: string|array{ // JSON Web Token configuration.
|
||||
* value?: scalar|Param|null, // JSON Web Token to use to publish to this hub.
|
||||
* provider?: scalar|Param|null, // The ID of a service to call to provide the JSON Web Token.
|
||||
|
||||
@@ -14,43 +14,60 @@ class ContactAutocompleteField extends AbstractType
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'class' => Contact::class,
|
||||
'placeholder' => '',
|
||||
'multiple' => true,
|
||||
'autocomplete' => true,
|
||||
'searchable_fields' => ['email', 'displayName'],
|
||||
'choice_label' => fn(Contact $c) => $c->getDisplayName()
|
||||
'class' => Contact::class,
|
||||
'placeholder' => '',
|
||||
'multiple' => true,
|
||||
'autocomplete' => true,
|
||||
'allow_options_create' => true,
|
||||
'searchable_fields' => ['email', 'displayName'],
|
||||
'choice_label' => fn(Contact $c) => $c->getDisplayName()
|
||||
? sprintf('%s <%s>', $c->getDisplayName(), $c->getEmail())
|
||||
: $c->getEmail(),
|
||||
|
||||
// Tom Select option + item renderers injected as JS strings.
|
||||
// UX Autocomplete passes these through to Tom Select's `render` config.
|
||||
'tom_select_options' => [
|
||||
'plugins' => ['remove_button'],
|
||||
'persist' => false,
|
||||
'closeAfterSelect' => false,
|
||||
'openOnFocus' => false,
|
||||
'hideSelected' => true,
|
||||
|
||||
'render' => [
|
||||
// Suggestion row in the dropdown
|
||||
// Dropdown suggestion row: avatar initial + name + email
|
||||
'option' => "function(data, escape) {
|
||||
var name = data.text ? data.text.replace(/<[^>]+>/g, '') : '';
|
||||
var email = data.value || '';
|
||||
// If choice_label formatted it as 'Name <email>', extract parts
|
||||
var match = name.match(/^(.+?) <([^>]+)>$/);
|
||||
var displayName = match ? escape(match[1]) : '';
|
||||
var displayEmail = match ? escape(match[2]) : escape(email);
|
||||
var initials = displayName
|
||||
var raw = data.text ? data.text.replace(/<[^>]+>/g, '') : (data.value || '');
|
||||
var match = raw.match(/^(.+?) <([^>]+)>$/);
|
||||
var displayName = match ? escape(match[1]) : '';
|
||||
var displayEmail = match ? escape(match[2]) : escape(raw);
|
||||
var initial = displayName
|
||||
? displayName.replace(/<[^>]*>/g, '').trim().split(/\\s+/).map(function(w){ return w[0]; }).slice(0,2).join('').toUpperCase()
|
||||
: displayEmail[0].toUpperCase();
|
||||
return '<div class=\"flex items-center gap-2.5 py-0.5\">'
|
||||
+ '<span class=\"ts-option-avatar\">' + initials + '</span>'
|
||||
: (displayEmail[0] || '?').toUpperCase();
|
||||
return '<div class=\"ts-option-row\">'
|
||||
+ '<span class=\"ts-option-avatar\">' + initial + '</span>'
|
||||
+ '<span class=\"ts-option-text\">'
|
||||
+ (displayName ? '<span class=\"ts-option-name\">' + displayName + '</span>' : '')
|
||||
+ '<span class=\"ts-option-email\">' + displayEmail + '</span>'
|
||||
+ '</span></div>';
|
||||
}",
|
||||
// Selected chip label
|
||||
|
||||
// Selected chip: name (or email) + × remove button
|
||||
'item' => "function(data, escape) {
|
||||
var name = data.text ? data.text.replace(/<[^>]+>/g, '') : data.value;
|
||||
var match = name.match(/^(.+?) <[^>]+>$/);
|
||||
var label = match ? escape(match[1]) : escape(name);
|
||||
return '<div>' + label + '</div>';
|
||||
var raw = data.text ? data.text.replace(/<[^>]+>/g, '') : (data.value || '');
|
||||
var match = raw.match(/^(.+?) <[^>]+>$/);
|
||||
var label = match ? escape(match[1]) : escape(raw);
|
||||
return '<div>' + label + '</div>';
|
||||
}",
|
||||
|
||||
// "Add <typed>" create row
|
||||
'option_create' => "function(data, escape) {
|
||||
return '<div class=\"ts-option-create\">'
|
||||
+ '<span class=\"ts-option-create-icon\">+</span>'
|
||||
+ 'Add <strong>' + escape(data.input) + '</strong>'
|
||||
+ '</div>';
|
||||
}",
|
||||
|
||||
// No results
|
||||
'no_results' => "function(data, escape) {
|
||||
return '<div class=\"ts-no-results\">No contacts found</div>';
|
||||
}",
|
||||
],
|
||||
],
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}{{ 'app.name'|trans }}{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 32 32%22><rect width=%2232%22 height=%2232%22 rx=%226%22 fill=%22%232563eb%22/><path d=%22M4 10 L16 19 L28 10 M4 10 L4 24 L28 24 L28 10 Z%22 stroke=%22white%22 stroke-width=%222.2%22 fill=%22none%22 stroke-linejoin=%22round%22/></svg>">
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 32 32%22><text y=%221em%22 font-size=%2224%22>✉️</text></svg>">
|
||||
|
||||
{# Must be first thing in <head> to avoid flash of wrong theme #}
|
||||
<script>
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
{# templates/form/compose_theme.html.twig #}
|
||||
{# templates/_layout/form/compose_theme.html.twig #}
|
||||
|
||||
{#
|
||||
address_entry_widget / address_entry_row are no longer used —
|
||||
the compose window uses ts-address fields directly.
|
||||
Kept here as no-ops so Symfony doesn't throw if they're referenced.
|
||||
Form theme for the compose window only.
|
||||
- Strips ALL styling from <select> elements so Tom Select copies no classes
|
||||
onto its .ts-wrapper div.
|
||||
- Removes the native-select chevron wrapper that modal_form_theme adds.
|
||||
- address_entry_widget / address_entry_row kept as no-ops.
|
||||
#}
|
||||
|
||||
{% block address_entry_widget %}{% endblock %}
|
||||
{% block address_entry_row %}{% endblock %}
|
||||
|
||||
{# ── Bare select — Tom Select clones this element's classes onto .ts-wrapper ── #}
|
||||
{% block choice_widget_collapsed %}
|
||||
<select {{ block('widget_attributes') }} {% if multiple %}multiple="multiple"{% endif %}>
|
||||
{% if placeholder is not none %}
|
||||
<option value=""{% if not value %} selected{% endif %}>{{ placeholder|trans({}, translation_domain) }}</option>
|
||||
{% endif %}
|
||||
{% if preferred_choices|length > 0 %}
|
||||
{% set options = preferred_choices %}
|
||||
{{ block('choice_widget_options') }}
|
||||
{% if choices|length > 0 and separator is not none %}
|
||||
<option disabled>{{ separator }}</option>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% set options = choices %}
|
||||
{{ block('choice_widget_options') }}
|
||||
</select>
|
||||
{% endblock %}
|
||||
|
||||
{% block email_widget %}
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
|
||||
{# ── Cc ───────────────────────────────────────────────── #}
|
||||
<div data-compose-target="ccField"
|
||||
class="hidden items-center gap-3 px-4 py-1.5 shrink-0 min-h-[36px]">
|
||||
class="flex hidden items-center gap-3 px-4 py-1.5 shrink-0 min-h-[36px]">
|
||||
<span class="text-xs font-medium text-gray-400 dark:text-gray-500 w-10 shrink-0 text-right">Cc</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
{{ form_widget(form.ccAddresses) }}
|
||||
@@ -151,7 +151,7 @@
|
||||
|
||||
{# ── Bcc ──────────────────────────────────────────────── #}
|
||||
<div data-compose-target="bccField"
|
||||
class="hidden items-center gap-3 px-4 py-1.5 shrink-0 min-h-[36px]">
|
||||
class="flex hidden items-center gap-3 px-4 py-1.5 shrink-0 min-h-[36px]">
|
||||
<span class="text-xs font-medium text-gray-400 dark:text-gray-500 w-10 shrink-0 text-right">Bcc</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
{{ form_widget(form.bccAddresses) }}
|
||||
|
||||
Reference in new issue
Block a user