fix(web): polls, recordings, reviews and reminders use @pi/ui's fields and buttons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -409,7 +409,8 @@ h3 {
|
|||||||
|
|
||||||
// A field's label floats above its box; other fields leave room for it
|
// A field's label floats above its box; other fields leave room for it
|
||||||
// below them (their hint line), a segmented control does not.
|
// below them (their hint line), a segmented control does not.
|
||||||
> pi-segmented-control {
|
> pi-segmented-control,
|
||||||
|
> p:has(+ pi-text-field, + pi-textarea, + pi-select-field, + pi-date-field) {
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,11 @@ export const APP_ICONS: Record<string, string> = {
|
|||||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>',
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>',
|
||||||
'user-profile-company':
|
'user-profile-company':
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V8l6-3v16M9 21h12V11l-6-2"/></svg>',
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V8l6-3v16M9 21h12V11l-6-2"/></svg>',
|
||||||
|
poll: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 20V12M12 20V5M19 20v-9M3 20h18"/></svg>',
|
||||||
|
video:
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="13" height="12" rx="2"/><path d="m16 10.5 5-3v9l-5-3"/></svg>',
|
||||||
|
mic: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21M9 21h6"/></svg>',
|
||||||
|
stop: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>',
|
||||||
whatsapp:
|
whatsapp:
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.4-5.2A8 8 0 1 1 21 12Z"/></svg>',
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.4-5.2A8 8 0 1 1 21 12Z"/></svg>',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -726,19 +726,6 @@ a.ts-key {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ts-resolve__label {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.375rem;
|
|
||||||
color: var(--ts-muted);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ts-resolve__text {
|
|
||||||
font: inherit;
|
|
||||||
color: var(--ts-ink);
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ts-reply__actions {
|
.ts-reply__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -782,59 +769,31 @@ a.ts-key {
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Small text buttons, not @pi/ui's sized ones. */
|
.ts-reminder__actions {
|
||||||
.ts-reminder__action {
|
display: inline-flex;
|
||||||
height: auto !important;
|
gap: 0.25rem;
|
||||||
min-width: 0 !important;
|
margin-left: auto;
|
||||||
padding: 0 !important;
|
|
||||||
border: 0;
|
|
||||||
background: none !important;
|
|
||||||
color: var(--ts-brand);
|
|
||||||
font: inherit;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ts-reminder__choices {
|
.ts-reminder__choices {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.375rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ts-reminder__choice {
|
.ts-reminder__custom {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
height: auto !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
padding: 0.625rem 0.875rem !important;
|
|
||||||
border: 1px solid var(--ts-line);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background: var(--ts-surface);
|
|
||||||
color: var(--ts-ink);
|
|
||||||
font: inherit;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
border-color: var(--ts-brand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ts-reminder__field {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.375rem;
|
grid-template-columns: minmax(0, 1fr) 8rem;
|
||||||
color: var(--ts-muted);
|
gap: 0.75rem;
|
||||||
font-size: 0.8125rem;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
.ts-input {
|
legend {
|
||||||
font: inherit;
|
margin-bottom: 1.25rem;
|
||||||
color: var(--ts-ink);
|
padding: 0;
|
||||||
|
color: var(--ts-ink-soft);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,45 +847,6 @@ a.ts-key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Small buttons, not @pi/ui's sized ones. */
|
|
||||||
.ts-reviews__button {
|
|
||||||
height: auto !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
padding: 0.25rem 0.75rem !important;
|
|
||||||
border: 1px solid var(--ts-line-strong) !important;
|
|
||||||
border-radius: 999px !important;
|
|
||||||
background: var(--ts-surface) !important;
|
|
||||||
color: var(--ts-ink);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.is-ok {
|
|
||||||
border-color: transparent !important;
|
|
||||||
background: var(--ts-c-green) !important;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
filter: brightness(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ts-reviews__link {
|
|
||||||
height: auto !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: 0;
|
|
||||||
background: none !important;
|
|
||||||
color: var(--ts-brand);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* A quick poll in a comment: each option a bar of its share. */
|
/* A quick poll in a comment: each option a bar of its share. */
|
||||||
.ts-poll {
|
.ts-poll {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -994,47 +914,44 @@ a.ts-key {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Small text buttons, not @pi/ui's sized ones. */
|
|
||||||
.ts-poll__close {
|
|
||||||
height: auto !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: 0;
|
|
||||||
background: none !important;
|
|
||||||
color: var(--ts-brand);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ts-poll-editor {
|
.ts-poll-editor {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.375rem;
|
gap: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.75rem;
|
padding: 1.25rem 1rem 1rem;
|
||||||
border: 1px dashed var(--ts-line-strong);
|
border: 1px solid var(--ts-line);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.75rem;
|
||||||
|
background: var(--ts-surface);
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.375rem;
|
||||||
color: var(--ts-muted);
|
color: var(--ts-ink-soft);
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ts-poll-editor__row {
|
.ts-poll-editor__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
|
|
||||||
.ts-input {
|
pi-text-field {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font: inherit;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ts-poll-editor__actions {
|
.ts-poll-editor__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Poll, screen and voice: what a comment can carry besides words. */
|
||||||
|
.ts-composer-tools {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,20 +318,22 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|||||||
@if (reminder.note) {
|
@if (reminder.note) {
|
||||||
· «{{ reminder.note }}»
|
· «{{ reminder.note }}»
|
||||||
}
|
}
|
||||||
<button
|
<span class="ts-reminder__actions">
|
||||||
type="button"
|
<pi-action-button
|
||||||
class="ts-reminder__action"
|
size="x-small"
|
||||||
(click)="openReminder()"
|
color="grey"
|
||||||
>
|
appearance="outline"
|
||||||
Cambiar
|
text="Cambiar"
|
||||||
</button>
|
(execute)="openReminder()"
|
||||||
<button
|
/>
|
||||||
type="button"
|
<pi-action-button
|
||||||
class="ts-reminder__action"
|
size="x-small"
|
||||||
(click)="facade.setReminder(null)"
|
color="grey"
|
||||||
>
|
appearance="ghost"
|
||||||
Quitar
|
text="Quitar"
|
||||||
</button>
|
(execute)="facade.setReminder(null)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,32 +384,34 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|||||||
}
|
}
|
||||||
<div class="ts-reviews__actions">
|
<div class="ts-reviews__actions">
|
||||||
@if (review.reviewer.id === facade.me()) {
|
@if (review.reviewer.id === facade.me()) {
|
||||||
<button
|
<pi-icon-button
|
||||||
type="button"
|
icon="check"
|
||||||
class="ts-reviews__button is-ok"
|
color="primary"
|
||||||
(click)="openReviewAnswer(review.id, approved)"
|
size="small"
|
||||||
>
|
text="Todo bien"
|
||||||
✓ Todo bien
|
(execute)="openReviewAnswer(review.id, approved)"
|
||||||
</button>
|
/>
|
||||||
<button
|
<pi-icon-button
|
||||||
type="button"
|
icon="edit"
|
||||||
class="ts-reviews__button"
|
color="grey"
|
||||||
(click)="openReviewAnswer(review.id, changes)"
|
appearance="outline"
|
||||||
>
|
size="small"
|
||||||
Pedir cambios
|
text="Pedir cambios"
|
||||||
</button>
|
(execute)="openReviewAnswer(review.id, changes)"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
@if (
|
@if (
|
||||||
review.requester.id === facade.me() ||
|
review.requester.id === facade.me() ||
|
||||||
project.canManage()
|
project.canManage()
|
||||||
) {
|
) {
|
||||||
<button
|
<pi-action-button
|
||||||
type="button"
|
size="small"
|
||||||
class="ts-reviews__link"
|
color="grey"
|
||||||
(click)="facade.cancelReview(review.id)"
|
appearance="ghost"
|
||||||
>
|
[hasPadding]="false"
|
||||||
Retirar
|
text="Retirar"
|
||||||
</button>
|
(execute)="facade.cancelReview(review.id)"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -968,20 +972,24 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|||||||
· cerrada{{
|
· cerrada{{
|
||||||
poll.winner === null ? ', sin ganadora' : ''
|
poll.winner === null ? ', sin ganadora' : ''
|
||||||
}}
|
}}
|
||||||
} @else if (
|
|
||||||
comment.author.id === facade.me() ||
|
|
||||||
project.canManage()
|
|
||||||
) {
|
|
||||||
·
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="ts-poll__close"
|
|
||||||
(click)="facade.closePoll(comment.id)"
|
|
||||||
>
|
|
||||||
Cerrar y decidir
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
|
@if (
|
||||||
|
!poll.closed &&
|
||||||
|
(comment.author.id === facade.me() ||
|
||||||
|
project.canManage())
|
||||||
|
) {
|
||||||
|
<div>
|
||||||
|
<pi-icon-button
|
||||||
|
icon="check"
|
||||||
|
color="grey"
|
||||||
|
appearance="outline"
|
||||||
|
size="small"
|
||||||
|
text="Cerrar y decidir"
|
||||||
|
(execute)="facade.closePoll(comment.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (
|
@if (
|
||||||
@@ -1072,31 +1080,27 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|||||||
"
|
"
|
||||||
novalidate
|
novalidate
|
||||||
>
|
>
|
||||||
<label class="ts-resolve__label">
|
<pi-textarea
|
||||||
Conclusión (opcional): quedará como decisión, arriba del
|
|
||||||
ticket
|
|
||||||
<textarea
|
|
||||||
class="ts-input ts-resolve__text"
|
|
||||||
rows="2"
|
|
||||||
maxlength="20000"
|
|
||||||
placeholder="Vamos con…"
|
placeholder="Vamos con…"
|
||||||
[value]="conclusion()"
|
[(value)]="conclusion"
|
||||||
(input)="conclusion.set($any($event.target).value)"
|
>
|
||||||
></textarea>
|
<pi-label
|
||||||
</label>
|
>Conclusión (opcional): quedará como decisión, arriba
|
||||||
|
del ticket</pi-label
|
||||||
|
>
|
||||||
|
</pi-textarea>
|
||||||
<div class="ts-reply__actions">
|
<div class="ts-reply__actions">
|
||||||
<pi-action-button
|
<pi-action-button
|
||||||
color="primary"
|
color="primary"
|
||||||
text="Resolver"
|
text="Resolver"
|
||||||
(execute)="resolve(thread.comment)"
|
(execute)="resolve(thread.comment)"
|
||||||
/>
|
/>
|
||||||
<button
|
<pi-action-button
|
||||||
type="button"
|
color="grey"
|
||||||
class="ts-plain-link"
|
appearance="ghost"
|
||||||
(click)="resolving.set(null)"
|
text="Cancelar"
|
||||||
>
|
(execute)="resolving.set(null)"
|
||||||
Cancelar
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
} @else if (replyingTo() === thread.comment.id) {
|
} @else if (replyingTo() === thread.comment.id) {
|
||||||
@@ -1211,64 +1215,71 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|||||||
/>
|
/>
|
||||||
@if (pollOn()) {
|
@if (pollOn()) {
|
||||||
<fieldset class="ts-poll-editor">
|
<fieldset class="ts-poll-editor">
|
||||||
<legend>Encuesta (2 a 6 opciones)</legend>
|
<legend>Encuesta · de 2 a 6 opciones</legend>
|
||||||
@for (option of pollOptions(); track $index) {
|
@for (option of pollOptions(); track $index) {
|
||||||
<div class="ts-poll-editor__row">
|
<div class="ts-poll-editor__row">
|
||||||
<input
|
<pi-text-field
|
||||||
class="ts-input"
|
[placeholder]="
|
||||||
maxlength="80"
|
$index === 0 ? 'Vamos con Stripe' : 'Otra opción'
|
||||||
[attr.aria-label]="'Opción ' + ($index + 1)"
|
|
||||||
[placeholder]="'Opción ' + ($index + 1)"
|
|
||||||
[value]="option"
|
|
||||||
(input)="
|
|
||||||
setPollOption($index, $any($event.target).value)
|
|
||||||
"
|
"
|
||||||
/>
|
[value]="option"
|
||||||
|
(valueChange)="setPollOption($index, $event)"
|
||||||
|
>
|
||||||
|
<pi-label>Opción {{ $index + 1 }}</pi-label>
|
||||||
|
</pi-text-field>
|
||||||
@if (pollOptions().length > 2) {
|
@if (pollOptions().length > 2) {
|
||||||
<button
|
<pi-icon-button
|
||||||
type="button"
|
icon="trash"
|
||||||
class="ts-poll__close"
|
color="grey"
|
||||||
|
appearance="ghost"
|
||||||
[attr.aria-label]="
|
[attr.aria-label]="
|
||||||
'Quitar la opción ' + ($index + 1)
|
'Quitar la opción ' + ($index + 1)
|
||||||
"
|
"
|
||||||
(click)="removePollOption($index)"
|
(execute)="removePollOption($index)"
|
||||||
>
|
/>
|
||||||
×
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="ts-poll-editor__actions">
|
<div class="ts-poll-editor__actions">
|
||||||
@if (pollOptions().length < 6) {
|
@if (pollOptions().length < 6) {
|
||||||
<button
|
<pi-icon-button
|
||||||
type="button"
|
icon="plus"
|
||||||
class="ts-poll__close"
|
color="grey"
|
||||||
(click)="pollOptions.set([...pollOptions(), ''])"
|
appearance="outline"
|
||||||
>
|
size="small"
|
||||||
+ Añadir opción
|
text="Añadir opción"
|
||||||
</button>
|
(execute)="pollOptions.set([...pollOptions(), ''])"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
<button
|
<pi-action-button
|
||||||
type="button"
|
size="small"
|
||||||
class="ts-poll__close"
|
color="grey"
|
||||||
(click)="pollOn.set(false)"
|
appearance="ghost"
|
||||||
>
|
[hasPadding]="false"
|
||||||
Quitar la encuesta
|
text="Quitar la encuesta"
|
||||||
</button>
|
(execute)="pollOn.set(false)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
} @else {
|
}
|
||||||
<div>
|
<div
|
||||||
<button
|
class="ts-composer-tools"
|
||||||
type="button"
|
role="group"
|
||||||
class="ts-poll__close"
|
aria-label="Añadir al comentario"
|
||||||
(click)="startPoll()"
|
|
||||||
>
|
>
|
||||||
📊 Añadir una encuesta
|
@if (!pollOn()) {
|
||||||
</button>
|
<pi-icon-button
|
||||||
</div>
|
icon="poll"
|
||||||
|
color="grey"
|
||||||
|
appearance="outline"
|
||||||
|
size="small"
|
||||||
|
text="Encuesta"
|
||||||
|
title="Una encuesta rápida: al cerrarla, la ganadora queda como decisión"
|
||||||
|
(execute)="startPoll()"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
<ts-media-recorder (recorded)="addRecording($event)" />
|
<ts-media-recorder (recorded)="addRecording($event)" />
|
||||||
|
</div>
|
||||||
<pi-file-upload
|
<pi-file-upload
|
||||||
label="Adjuntar al comentario"
|
label="Adjuntar al comentario"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
@@ -1449,30 +1460,20 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|||||||
<p class="ts-muted">
|
<p class="ts-muted">
|
||||||
Le llegará un aviso y lo verá en «Mi trabajo» hasta que responda.
|
Le llegará un aviso y lo verá en «Mi trabajo» hasta que responda.
|
||||||
</p>
|
</p>
|
||||||
<label class="ts-reminder__field">
|
<pi-select-field
|
||||||
Quién
|
placeholder="Elige a alguien…"
|
||||||
<select
|
[options]="reviewerOptions()"
|
||||||
class="ts-input"
|
[value]="reviewer() || null"
|
||||||
[value]="reviewer()"
|
(valueChange)="reviewer.set($event ?? '')"
|
||||||
(change)="reviewer.set($any($event.target).value)"
|
|
||||||
>
|
>
|
||||||
<option value="">Elige a alguien…</option>
|
<pi-label [isRequired]="true">Quién</pi-label>
|
||||||
@for (member of reviewers(); track member.id) {
|
</pi-select-field>
|
||||||
<option [value]="member.id">{{ member.name }}</option>
|
<pi-textarea
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="ts-reminder__field">
|
|
||||||
Qué mirar (opcional)
|
|
||||||
<textarea
|
|
||||||
class="ts-input ts-resolve__text"
|
|
||||||
rows="3"
|
|
||||||
maxlength="1000"
|
|
||||||
placeholder="Revisa el flujo en móvil, sobre todo el pago"
|
placeholder="Revisa el flujo en móvil, sobre todo el pago"
|
||||||
[value]="reviewNote()"
|
[(value)]="reviewNote"
|
||||||
(input)="reviewNote.set($any($event.target).value)"
|
>
|
||||||
></textarea>
|
<pi-label>Qué mirar (opcional)</pi-label>
|
||||||
</label>
|
</pi-textarea>
|
||||||
<pi-action-button
|
<pi-action-button
|
||||||
size="w100"
|
size="w100"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -1489,20 +1490,20 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|||||||
(submit)="$event.preventDefault(); sendReviewAnswer()"
|
(submit)="$event.preventDefault(); sendReviewAnswer()"
|
||||||
novalidate
|
novalidate
|
||||||
>
|
>
|
||||||
<label class="ts-reminder__field">
|
<pi-textarea
|
||||||
{{
|
[placeholder]="
|
||||||
|
answering()?.verdict === approved
|
||||||
|
? 'Probado en móvil y escritorio'
|
||||||
|
: 'El botón de pagar no responde en Safari'
|
||||||
|
"
|
||||||
|
[(value)]="reviewAnswer"
|
||||||
|
>
|
||||||
|
<pi-label [isRequired]="answering()?.verdict === changes">{{
|
||||||
answering()?.verdict === approved
|
answering()?.verdict === approved
|
||||||
? 'Un comentario (opcional)'
|
? 'Un comentario (opcional)'
|
||||||
: 'Qué hay que cambiar'
|
: 'Qué hay que cambiar'
|
||||||
}}
|
}}</pi-label>
|
||||||
<textarea
|
</pi-textarea>
|
||||||
class="ts-input ts-resolve__text"
|
|
||||||
rows="4"
|
|
||||||
maxlength="1000"
|
|
||||||
[value]="reviewAnswer()"
|
|
||||||
(input)="reviewAnswer.set($any($event.target).value)"
|
|
||||||
></textarea>
|
|
||||||
</label>
|
|
||||||
<pi-action-button
|
<pi-action-button
|
||||||
size="w100"
|
size="w100"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -1522,7 +1523,7 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|||||||
<ng-template #reminderForm>
|
<ng-template #reminderForm>
|
||||||
<form
|
<form
|
||||||
class="ts-form"
|
class="ts-form"
|
||||||
(submit)="$event.preventDefault(); saveReminder(reminderAt())"
|
(submit)="$event.preventDefault(); saveCustomReminder()"
|
||||||
novalidate
|
novalidate
|
||||||
>
|
>
|
||||||
<p class="ts-muted">
|
<p class="ts-muted">
|
||||||
@@ -1531,42 +1532,40 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|||||||
</p>
|
</p>
|
||||||
<div class="ts-reminder__choices" role="group" aria-label="Cuándo">
|
<div class="ts-reminder__choices" role="group" aria-label="Cuándo">
|
||||||
@for (choice of reminderChoices(); track choice.label) {
|
@for (choice of reminderChoices(); track choice.label) {
|
||||||
<button
|
<pi-action-button
|
||||||
type="button"
|
size="w100"
|
||||||
class="ts-reminder__choice"
|
color="grey"
|
||||||
(click)="saveReminder(choice.at)"
|
appearance="outline"
|
||||||
>
|
[text]="choice.label + ' · ' + when(choice.at)"
|
||||||
<span>{{ choice.label }}</span>
|
(execute)="saveReminder(choice.at)"
|
||||||
<span class="ts-muted">{{ when(choice.at) }}</span>
|
/>
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<label class="ts-reminder__field">
|
<fieldset class="ts-reminder__custom">
|
||||||
Otro momento
|
<legend>Otro momento</legend>
|
||||||
<input
|
<pi-date-field [min]="today()" [(value)]="reminderDay">
|
||||||
class="ts-input"
|
<pi-label>Día</pi-label>
|
||||||
type="datetime-local"
|
</pi-date-field>
|
||||||
[min]="nowLocal()"
|
<pi-select-field
|
||||||
[value]="reminderAt()"
|
[options]="timeOptions"
|
||||||
(input)="reminderAt.set($any($event.target).value)"
|
[value]="reminderTime()"
|
||||||
/>
|
(valueChange)="reminderTime.set($event ?? '09:00')"
|
||||||
</label>
|
>
|
||||||
<label class="ts-reminder__field">
|
<pi-label>Hora</pi-label>
|
||||||
Nota (opcional)
|
</pi-select-field>
|
||||||
<input
|
</fieldset>
|
||||||
class="ts-input"
|
<pi-text-field
|
||||||
maxlength="280"
|
|
||||||
placeholder="Llamar a QA antes de la release"
|
placeholder="Llamar a QA antes de la release"
|
||||||
[value]="reminderNote()"
|
[(value)]="reminderNote"
|
||||||
(input)="reminderNote.set($any($event.target).value)"
|
>
|
||||||
/>
|
<pi-label>Nota (opcional)</pi-label>
|
||||||
</label>
|
</pi-text-field>
|
||||||
<pi-action-button
|
<pi-action-button
|
||||||
size="w100"
|
size="w100"
|
||||||
color="primary"
|
color="primary"
|
||||||
text="Recordármelo"
|
text="Recordármelo"
|
||||||
[disabled]="!reminderAt()"
|
[disabled]="!customReminder()"
|
||||||
(execute)="saveReminder(reminderAt())"
|
(execute)="saveCustomReminder()"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -1698,9 +1697,23 @@ export class IssuePage {
|
|||||||
)
|
)
|
||||||
.map((m) => ({ id: m.user.id, name: m.user.name })),
|
.map((m) => ({ id: m.user.id, name: m.user.name })),
|
||||||
);
|
);
|
||||||
/** The reminder being written: `datetime-local` value and note. */
|
protected readonly reviewerOptions = computed<PiSelectFieldOption<string>[]>(
|
||||||
protected readonly reminderAt = signal('');
|
() => this.reviewers().map((m) => ({ value: m.id, label: m.name })),
|
||||||
|
);
|
||||||
|
/** The reminder being written: day, time and note. */
|
||||||
|
protected readonly reminderDay = signal<Date | null>(null);
|
||||||
|
protected readonly reminderTime = signal('09:00');
|
||||||
protected readonly reminderNote = signal('');
|
protected readonly reminderNote = signal('');
|
||||||
|
protected readonly timeOptions = TIME_OPTIONS;
|
||||||
|
/** The day and time chosen, if both make a moment still to come. */
|
||||||
|
protected readonly customReminder = computed(() => {
|
||||||
|
const day = this.reminderDay();
|
||||||
|
if (!day) return null;
|
||||||
|
const [hours, minutes] = this.reminderTime().split(':').map(Number);
|
||||||
|
const at = new Date(day);
|
||||||
|
at.setHours(hours, minutes, 0, 0);
|
||||||
|
return at.getTime() > Date.now() ? at : null;
|
||||||
|
});
|
||||||
protected readonly reminderChoices = signal<ReminderChoice[]>([]);
|
protected readonly reminderChoices = signal<ReminderChoice[]>([]);
|
||||||
protected readonly when = whenLabel;
|
protected readonly when = whenLabel;
|
||||||
private readonly editForm =
|
private readonly editForm =
|
||||||
@@ -2158,12 +2171,18 @@ export class IssuePage {
|
|||||||
protected openReminder(): void {
|
protected openReminder(): void {
|
||||||
const current = this.facade.issue()?.myReminder;
|
const current = this.facade.issue()?.myReminder;
|
||||||
this.reminderChoices.set(reminderChoices());
|
this.reminderChoices.set(reminderChoices());
|
||||||
this.reminderAt.set(current ? toLocalInput(new Date(current.at)) : '');
|
const at = current ? new Date(current.at) : null;
|
||||||
|
this.reminderDay.set(at);
|
||||||
|
this.reminderTime.set(at ? halfHourOf(at) : '09:00');
|
||||||
this.reminderNote.set(current?.note ?? '');
|
this.reminderNote.set(current?.note ?? '');
|
||||||
this.drawer.open('Recordármelo', this.reminderForm());
|
this.drawer.open('Recordármelo', this.reminderForm());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `at`: ISO, or the `datetime-local` value (local time). */
|
protected saveCustomReminder(): void {
|
||||||
|
const at = this.customReminder();
|
||||||
|
if (at) this.saveReminder(at.toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
protected saveReminder(at: string): void {
|
protected saveReminder(at: string): void {
|
||||||
if (!at) return;
|
if (!at) return;
|
||||||
const when = new Date(at);
|
const when = new Date(at);
|
||||||
@@ -2172,9 +2191,11 @@ export class IssuePage {
|
|||||||
this.drawer.closeAll();
|
this.drawer.closeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Now, as the `min` of a `datetime-local` input. */
|
/** The first day a reminder can be set on. */
|
||||||
protected nowLocal(): string {
|
protected today(): Date {
|
||||||
return toLocalInput(new Date());
|
const day = new Date();
|
||||||
|
day.setHours(0, 0, 0, 0);
|
||||||
|
return day;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected move(to: string): void {
|
protected move(to: string): void {
|
||||||
@@ -2303,7 +2324,7 @@ export class IssuePage {
|
|||||||
|
|
||||||
protected setPollOption(index: number, value: string): void {
|
protected setPollOption(index: number, value: string): void {
|
||||||
this.pollOptions.update((options) =>
|
this.pollOptions.update((options) =>
|
||||||
options.map((o, i) => (i === index ? value : o)),
|
options.map((o, i) => (i === index ? value.slice(0, 80) : o)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2548,7 +2569,18 @@ export class IssuePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** A date as a `datetime-local` value (local time, to the minute). */
|
/** A date as a `datetime-local` value (local time, to the minute). */
|
||||||
function toLocalInput(date: Date): string {
|
|
||||||
const pad = (n: number) => String(n).padStart(2, '0');
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
||||||
|
/** The times a reminder can ring at: every half hour. */
|
||||||
|
const TIME_OPTIONS: PiSelectFieldOption<string>[] = Array.from(
|
||||||
|
{ length: 48 },
|
||||||
|
(_, i) => {
|
||||||
|
const time = `${pad(Math.floor(i / 2))}:${i % 2 ? '30' : '00'}`;
|
||||||
|
return { value: time, label: time };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** A moment's time, to the half hour before it. */
|
||||||
|
function halfHourOf(date: Date): string {
|
||||||
|
return `${pad(date.getHours())}:${date.getMinutes() < 30 ? '00' : '30'}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
output,
|
output,
|
||||||
signal,
|
signal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { PiIconButtonComponent } from '@pi/ui';
|
||||||
import { ToastService } from '@task-stream/web/shared/ui';
|
import { ToastService } from '@task-stream/web/shared/ui';
|
||||||
|
|
||||||
/** Long enough to show a bug, short enough to stay under the upload limit. */
|
/** Long enough to show a bug, short enough to stay under the upload limit. */
|
||||||
@@ -42,68 +43,65 @@ function mimeFor(kind: Kind): string {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ts-media-recorder',
|
selector: 'ts-media-recorder',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [PiIconButtonComponent],
|
||||||
template: `
|
template: `
|
||||||
@if (recording(); as kind) {
|
@if (recording(); as kind) {
|
||||||
<span class="ts-recorder__live" role="status">
|
<span class="ts-recorder__live" role="status">
|
||||||
<span class="ts-recorder__dot" aria-hidden="true"></span>
|
<span class="ts-recorder__dot" aria-hidden="true"></span>
|
||||||
Grabando {{ kind === 'screen' ? 'la pantalla' : 'tu voz' }} ·
|
Grabando {{ kind === 'screen' ? 'la pantalla' : 'tu voz' }}
|
||||||
{{ clock(elapsed()) }} / {{ clock(max) }}
|
<span class="ts-recorder__clock"
|
||||||
|
>{{ clock(elapsed()) }} / {{ clock(max) }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<button type="button" class="ts-recorder__button" (click)="stop()">
|
<pi-icon-button
|
||||||
■ Parar
|
icon="stop"
|
||||||
</button>
|
color="warn"
|
||||||
|
appearance="outline"
|
||||||
|
size="small"
|
||||||
|
text="Parar"
|
||||||
|
(execute)="stop()"
|
||||||
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
@if (canScreen) {
|
@if (canScreen) {
|
||||||
<button
|
<pi-icon-button
|
||||||
type="button"
|
icon="video"
|
||||||
class="ts-recorder__button"
|
color="grey"
|
||||||
|
appearance="outline"
|
||||||
|
size="small"
|
||||||
|
text="Grabar pantalla"
|
||||||
title="Graba la pantalla (y tu voz, si das permiso al micrófono) y lo adjunta al comentario"
|
title="Graba la pantalla (y tu voz, si das permiso al micrófono) y lo adjunta al comentario"
|
||||||
(click)="start('screen')"
|
(execute)="start('screen')"
|
||||||
>
|
/>
|
||||||
🎥 Grabar pantalla
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
@if (canVoice) {
|
@if (canVoice) {
|
||||||
<button
|
<pi-icon-button
|
||||||
type="button"
|
icon="mic"
|
||||||
class="ts-recorder__button"
|
color="grey"
|
||||||
|
appearance="outline"
|
||||||
|
size="small"
|
||||||
|
text="Grabar audio"
|
||||||
title="Graba un audio y lo adjunta al comentario"
|
title="Graba un audio y lo adjunta al comentario"
|
||||||
(click)="start('voice')"
|
(execute)="start('voice')"
|
||||||
>
|
/>
|
||||||
🎙 Grabar audio
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: contents;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
/* Small text buttons, not @pi/ui's sized ones. */
|
|
||||||
.ts-recorder__button {
|
|
||||||
height: auto !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: 0;
|
|
||||||
background: none !important;
|
|
||||||
color: var(--ts-brand);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.ts-recorder__live {
|
.ts-recorder__live {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.8125rem;
|
padding: 0 0.75rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, var(--ts-c-red) 12%, transparent);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.ts-recorder__clock {
|
||||||
|
font-weight: 400;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.ts-recorder__dot {
|
.ts-recorder__dot {
|
||||||
|
|||||||
Reference in New Issue
Block a user