icon-selector/custom-icon/add-custom-icon-modal/add-custom-icon-modal.component.ts
| selector | c8y-add-custom-icon-modal |
| imports |
C8yTranslatePipe
TooltipModule
|
| templateUrl | ./add-custom-icon-modal.component.html |
No results matching.
<c8y-modal
[title]="modalTitle() | translate"
[headerClasses]="'dialog-header'"
(onDismiss)="cancel()"
(onClose)="save()"
[close]="keepOpenOnOk"
[labels]="{ cancel: 'Cancel', ok: okLabel() | translate }"
>
<ng-container c8y-modal-title>
<span
[c8yIcon]="isOverrideMode() ? 'replace' : isEditMode() ? 'pencil' : 'plus-circle-o'"
aria-hidden="true"
></span>
</ng-container>
@if (!isReady()) {
<!-- min-height matches the approximate form height so Bootstrap animates
the modal at its full size before the form is rendered. -->
<div
class="p-32 d-flex a-i-center j-c-center"
style="min-height: 380px"
[attr.aria-label]="'Loading…' | translate"
role="status"
>
<c8y-loading></c8y-loading>
</div>
}
@if (isReady()) {
<div
class="p-t-16 p-b-24 p-l-32 p-r-32"
#modalBody
>
@if (isOverrideMode()) {
<p class="text-center text-16 text-bold text-balance m-b-16">
{{ 'Upload a replacement SVG for {{iconName}}' | translate: { iconName:
editingIcon?.overriddenIconName } }}
</p>
}
<!-- Name, collection and icon type (hidden in override mode) -->
@if (!isOverrideMode()) {
@if (!isEditMode()) {
<p
class="text-muted m-b-16"
translate
>
Upload a custom SVG icon and assign it to a collection to keep your icons organized.
</p>
}
<div
class="m-b-16"
[formGroup]="form"
>
<c8y-form-group class="p-b-16">
<label
for="iconName"
translate
>Icon name</label
>
<input
class="form-control"
id="iconName"
aria-required="true"
aria-describedby="iconName-messages"
type="text"
[placeholder]="'e.g. {{ example }}' | translate: { example: 'acme-logo' }"
formControlName="name"
/>
<c8y-messages
id="iconName-messages"
role="status"
[helpMessage]="'Lowercase letters, numbers and hyphens only' | translate"
>
@for (message of nameMessages; track message.name) {
<c8y-message
[name]="message.name"
[text]="message.text | translate"
></c8y-message>
}
</c8y-messages>
</c8y-form-group>
<c8y-form-group [hasError]="hasCategoryError()">
<label
for="iconCategory"
translate
>Collection</label
>
<div class="p-relative">
<c8y-typeahead
[attr.aria-label]="'Icon collection' | translate"
id="iconCategory"
#categoryTypeahead
[allowFreeEntries]="true"
[hideNew]="true"
[placeholder]="'Select or enter a collection name' | translate"
(onSearch)="onCategorySearch($event)"
>
@for (cat of filteredCategories(); track cat) {
<c8y-li
class="p-l-8 p-r-8 c8y-list__item--link"
(click)="selectCategory(cat)"
[active]="form.value.category === cat"
>{{ cat | translate }}</c8y-li
>
}
@if (showAddNew()) {
<c8y-li class="bg-level-2 p-8">
@if (hasNoMatch()) {
<span
class="m-r-8"
translate
>No existing collection found.</span
>
}
<button
class="btn btn-default btn-xs"
type="button"
(click)="selectCategory(categorySearch())"
>
{{ 'Add new collection`collection`' | translate }}
</button>
</c8y-li>
}
</c8y-typeahead>
@if (categorySearch()) {
<button
class="btn btn-dot p-absolute"
style="right: 36px; z-index: 10"
[attr.aria-label]="'Clear collection' | translate"
type="button"
(click)="clearCategory()"
>
<i
[c8yIcon]="'times'"
aria-hidden="true"
></i>
</button>
}
</div>
<c8y-messages
role="alert"
[show]="categoryErrors()"
>
<c8y-message
[name]="'required'"
[text]="'Collection is required.' | translate"
></c8y-message>
</c8y-messages>
</c8y-form-group>
@if (!isEditMode()) {
<fieldset class="c8y-fieldset m-b-24">
<legend translate>Icon type</legend>
<div class="d-flex gap-16 p-b-16">
<label class="c8y-radio a-i-start">
<input
name="iconFamily"
type="radio"
[checked]="selectedFamily() === 'dlt'"
(change)="onFamilyChange('dlt')"
/>
<span></span>
<span
>{{ 'Single color' | translate }}<br />
<small
class="text-muted d-block"
translate
>
Single SVG, inherits text color via currentColor.
</small>
</span>
</label>
<label class="c8y-radio a-i-start">
<input
name="iconFamily"
type="radio"
[checked]="selectedFamily() === 'c8y'"
(change)="onFamilyChange('c8y')"
/>
<span></span>
<span>
{{ 'Duocolor' | translate }}<br />
<small
class="text-muted d-block"
translate
>
Two SVGs required: a dark layer and a light accent layer.
</small>
</span>
</label>
</div>
</fieldset>
}
</div>
}
<!-- SVG uploads -->
<div
class="m-b-16"
[class.row]="selectedFamily() === 'c8y'"
>
@if (selectedFamily() === 'dlt') {
<c8y-form-group [hasError]="hasPrimarySvgError()">
<label translate>Icon SVG</label>
@if (!primarySvg()) {
<c8y-drop-area
class="drop-area-sm"
[icon]="'upload'"
[title]="'Upload SVG icon' | translate"
[attr.aria-label]="'Upload SVG icon' | translate"
[accept]="'.svg'"
[maxAllowedFiles]="1"
[maxFileSizeInMegaBytes]="5"
[message]="'Drop SVG file here or click to browse' | translate"
(dropped)="onPrimaryDropped($event)"
></c8y-drop-area>
<c8y-messages
class="m-t-8"
role="status"
[show]="primarySvgErrors()"
[helpMessage]="
hasPrimarySvgError()
? ''
: ('Square viewBox (e.g. 24×24) · currentColor fills · no external refs or scripts'
| translate)
"
>
<c8y-message
[name]="'required'"
[text]="'Upload an SVG file.' | translate"
></c8y-message>
@if (primaryValidation(); as pv) {
@for (error of pv.errors; track error) {
<div class="d-block text-danger">{{ error }}</div>
}
@for (warning of pv.warnings; track warning) {
<div class="d-block text-warning">{{ warning }}</div>
}
}
</c8y-messages>
} @else {
<fieldset class="c8y-fieldset m-b-8">
<legend class="d-flex a-i-center">
<span translate>Preview</span>
<button
class="btn btn-dot btn-dot--danger m-l-8"
[attr.aria-label]="'Remove SVG preview' | translate"
[tooltip]="'Remove SVG preview' | translate"
placement="top"
container="body"
[delay]="500"
(click)="clearPrimary()"
>
<i
[c8yIcon]="'minus-circle'"
aria-hidden="true"
></i>
</button>
</legend>
<div class="row d-flex a-i-center gap-16 p-b-16">
<div
class="col-sm-6 d-flex a-i-end gap-8 j-c-around p-16 c8y-light-theme"
style="
background: var(--c8y-root-component-background-default);
border: 1px solid var(--c8y-root-component-border-color);
border-radius: var(--c8y-component-border-radius-base, 4px);
"
>
@for (size of previewSizes; track size) {
<div class="d-flex d-col a-i-center">
<div
class="d-flex a-i-center j-c-center text-default"
[style.width.px]="size"
[style.height.px]="size"
aria-hidden="true"
[innerHTML]="primaryPreviewHtml()"
></div>
<small class="text-muted">{{ size }}</small>
</div>
}
</div>
<div
class="col-sm-6 d-flex a-i-end gap-16 j-c-around p-16 c8y-dark-theme"
style="
background: var(--c8y-root-component-background-default);
border: 1px solid var(--c8y-root-component-border-color);
border-radius: var(--c8y-component-border-radius-base, 4px);
"
>
@for (size of previewSizes; track size) {
<div class="d-flex d-col a-i-center">
<div
class="d-flex a-i-center j-c-center text-default"
[style.width.px]="size"
[style.height.px]="size"
aria-hidden="true"
[innerHTML]="primaryPreviewHtml()"
></div>
<small class="text-muted">{{ size }}</small>
</div>
}
</div>
</div>
@if (primaryValidation(); as pv) {
@if (pv.errors.length || pv.warnings.length) {
<small
class="form-control-feedback-message"
role="status"
>
@for (error of pv.errors; track error) {
<div class="d-block text-danger">{{ error }}</div>
}
@for (warning of pv.warnings; track warning) {
<div class="d-block text-warning">{{ warning }}</div>
}
</small>
}
}
</fieldset>
}
</c8y-form-group>
} @else {
<!-- C8Y duocolor: dark + light uploads side by side -->
<div class="row d-flex-md gap-16">
<div class="col-sm-6">
<c8y-form-group [hasError]="hasPrimarySvgError()">
<label translate>Dark layer SVG</label>
@if (!primarySvg()) {
<c8y-drop-area
class="drop-area-sm"
[icon]="'upload'"
[attr.aria-label]="'Upload dark layer SVG' | translate"
[accept]="'.svg'"
[maxAllowedFiles]="1"
[maxFileSizeInMegaBytes]="5"
[message]="'Drop dark layer SVG' | translate"
(dropped)="onPrimaryDropped($event)"
></c8y-drop-area>
<c8y-messages
class="m-t-8"
role="status"
[show]="primarySvgErrors()"
[helpMessage]="
hasPrimarySvgError()
? ''
: ('Square viewBox · currentColor fills · main icon shape' | translate)
"
>
<c8y-message
[name]="'required'"
[text]="'Upload an SVG file.' | translate"
></c8y-message>
@if (primaryValidation(); as pv) {
@for (error of pv.errors; track error) {
<div class="d-block text-danger">{{ error }}</div>
}
@for (warning of pv.warnings; track warning) {
<div class="d-block text-warning">{{ warning }}</div>
}
}
</c8y-messages>
} @else {
<div class="d-flex a-i-center gap-8 p-8 separator m-b-4">
<div
class="d-flex a-i-center j-c-center"
[style.width.px]="32"
[style.height.px]="32"
aria-hidden="true"
[innerHTML]="primaryPreviewHtml()"
></div>
<span
class="text-muted text-12 flex-grow"
translate
>Dark layer</span
>
<button
class="btn btn-dot btn-dot--danger"
[attr.aria-label]="'Remove dark layer SVG' | translate"
[tooltip]="'Remove dark layer SVG' | translate"
placement="top"
container="body"
[delay]="500"
(click)="clearPrimary()"
>
<i
[c8yIcon]="'minus-circle'"
aria-hidden="true"
></i>
</button>
</div>
@if (primaryValidation(); as pv) {
@if (pv.errors.length || pv.warnings.length) {
<small
class="form-control-feedback-message"
role="status"
>
@for (error of pv.errors; track error) {
<div class="d-block text-danger">{{ error }}</div>
}
@for (warning of pv.warnings; track warning) {
<div class="d-block text-warning">{{ warning }}</div>
}
</small>
}
}
}
</c8y-form-group>
</div>
<div class="col-sm-6">
<c8y-form-group [hasError]="hasLightSvgError()">
<label translate>Light layer SVG</label>
@if (!lightSvg()) {
<c8y-drop-area
class="drop-area-sm"
[icon]="'upload'"
[attr.aria-label]="'Upload light layer SVG' | translate"
[accept]="'.svg'"
[maxAllowedFiles]="1"
[maxFileSizeInMegaBytes]="5"
[message]="'Drop light layer SVG' | translate"
(dropped)="onLightDropped($event)"
></c8y-drop-area>
<c8y-messages
class="m-t-8"
role="status"
[show]="lightSvgErrors()"
[helpMessage]="
hasLightSvgError()
? ''
: ('Same viewBox as dark layer · accent shape for the light tint'
| translate)
"
>
<c8y-message
[name]="'required'"
[text]="'Upload an SVG file.' | translate"
></c8y-message>
@if (lightValidation(); as lv) {
@for (error of lv.errors; track error) {
<div class="d-block text-danger">{{ error }}</div>
}
@for (warning of lv.warnings; track warning) {
<div class="d-block text-warning">{{ warning }}</div>
}
}
</c8y-messages>
} @else {
<div class="d-flex a-i-center gap-8 p-8 separator m-b-4">
<div
class="d-flex a-i-center j-c-center"
[style.width.px]="32"
[style.height.px]="32"
aria-hidden="true"
[innerHTML]="lightPreviewHtml()"
></div>
<span
class="text-muted text-12 flex-grow"
translate
>Light layer</span
>
<button
class="btn btn-dot btn-dot--danger"
[attr.aria-label]="'Remove light layer SVG' | translate"
[tooltip]="'Remove light layer SVG' | translate"
placement="top"
container="body"
[delay]="500"
(click)="clearLight()"
>
<i
[c8yIcon]="'minus-circle'"
aria-hidden="true"
></i>
</button>
</div>
@if (lightValidation(); as lv) {
@if (lv.errors.length || lv.warnings.length) {
<small
class="form-control-feedback-message"
role="status"
>
@for (error of lv.errors; track error) {
<div class="d-block text-danger">{{ error }}</div>
}
@for (warning of lv.warnings; track warning) {
<div class="d-block text-warning">{{ warning }}</div>
}
</small>
}
}
}
</c8y-form-group>
</div>
</div>
@if (showCompositePreview()) {
<fieldset
class="c8y-fieldset m-t-16"
aria-hidden="true"
>
<legend translate>Composited preview</legend>
<div class="row d-flex-md gap-16 p-b-16">
<!-- Light background -->
<div
class="col-sm-6 d-flex a-i-end gap-8 j-c-around p-16 c8y-light-theme"
style="
background: var(--c8y-root-component-background-default);
border: 1px solid var(--c8y-root-component-border-color);
border-radius: var(--c8y-component-border-radius-base, 4px);
"
>
@for (size of previewSizes; track size) {
<div class="d-flex d-col a-i-center">
<div
class="p-relative"
[style.width.px]="size"
[style.height.px]="size"
>
<div
class="d-flex a-i-center j-c-center fit-w fit-h"
style="color: var(--c8y-root-component-icon-dark-color-dark)"
[innerHTML]="primaryPreviewHtml()"
></div>
<div
class="d-flex a-i-center j-c-center fit-w fit-h p-absolute"
style="
top: 0;
left: 0;
color: var(--c8y-root-component-icon-dark-color-light);
"
[innerHTML]="lightPreviewHtml()"
></div>
</div>
<small class="text-muted">{{ size }}</small>
</div>
}
</div>
<!-- Dark background -->
<div
class="col-sm-6 d-flex a-i-end gap-8 j-c-around p-16 c8y-dark-theme"
style="
background: var(--c8y-root-component-background-default);
border-radius: var(--c8y-component-border-radius-base, 4px);
"
>
@for (size of previewSizes; track size) {
<div class="d-flex d-col a-i-center">
<div
class="p-relative"
[style.width.px]="size"
[style.height.px]="size"
>
<div
class="d-flex a-i-center j-c-center fit-w fit-h"
style="color: var(--c8y-root-component-icon-dark-color-dark)"
[innerHTML]="primaryPreviewHtml()"
></div>
<div
class="d-flex a-i-center j-c-center fit-w fit-h p-absolute"
style="
top: 0;
left: 0;
color: var(
--c8y-component-icon-dark-color-light,
var(--c8y-root-component-icon-dark-color-light)
);
"
[innerHTML]="lightPreviewHtml()"
></div>
</div>
<small class="text-muted">{{ size }}</small>
</div>
}
</div>
</div>
</fieldset>
}
}
</div>
</div>
}
</c8y-modal>