41 Commits

Author SHA1 Message Date
fa2e249e94 Merge pull request 'feat(front-end): linkedin logo' (#56) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 30s
Build and Deploy / test-frontend (push) Successful in 1m5s
Build and Deploy / build-and-push (push) Successful in 18s
Build and Deploy / deploy (push) Successful in 22s
Reviewed-on: #56
2026-03-25 19:26:41 +01:00
132f0f3646 feat(front-end): linkedin logo
All checks were successful
Build and Deploy / test-backend (push) Successful in 37s
Build and Deploy / test-frontend (push) Successful in 1m4s
Build and Deploy / build-and-push (push) Successful in 36s
Build and Deploy / deploy (push) Successful in 22s
PR Checks / prettier-autofix (pull_request) Successful in 8s
PR Checks / security-sast (pull_request) Successful in 35s
PR Checks / test-backend (pull_request) Successful in 30s
PR Checks / test-frontend (pull_request) Successful in 1m6s
2026-03-25 19:18:38 +01:00
3cbcec5f53 Merge pull request 'dev' (#55) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 29s
Build and Deploy / test-frontend (push) Successful in 1m46s
Build and Deploy / deploy (push) Successful in 35s
Build and Deploy / build-and-push (push) Successful in 1m5s
Reviewed-on: #55
2026-03-25 11:57:49 +01:00
printcalc-ci
8835175fb3 style: apply prettier formatting
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 12s
PR Checks / security-sast (pull_request) Successful in 52s
PR Checks / test-frontend (pull_request) Successful in 1m17s
PR Checks / test-backend (pull_request) Successful in 27s
2026-03-25 10:46:11 +00:00
28c3abdb4a Merge branch 'main' into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 28s
PR Checks / prettier-autofix (pull_request) Successful in 14s
Build and Deploy / test-frontend (push) Successful in 1m41s
PR Checks / security-sast (pull_request) Successful in 52s
PR Checks / test-backend (pull_request) Successful in 30s
Build and Deploy / build-and-push (push) Successful in 32s
Build and Deploy / deploy (push) Successful in 20s
PR Checks / test-frontend (pull_request) Successful in 1m42s
2026-03-25 11:44:01 +01:00
b30bfc9293 fix(front-end): improvements in load products by uuid truncated
Some checks failed
Build and Deploy / test-backend (push) Successful in 38s
Build and Deploy / test-frontend (push) Successful in 1m37s
Build and Deploy / build-and-push (push) Successful in 1m57s
Build and Deploy / deploy (push) Successful in 22s
PR Checks / prettier-autofix (pull_request) Failing after 13s
PR Checks / test-backend (pull_request) Successful in 28s
PR Checks / security-sast (pull_request) Successful in 40s
PR Checks / test-frontend (pull_request) Successful in 1m15s
2026-03-25 11:27:43 +01:00
d70423fcc0 fix(front-end): improvements in ssr
All checks were successful
Build and Deploy / test-backend (push) Successful in 34s
Build and Deploy / test-frontend (push) Successful in 1m6s
Build and Deploy / build-and-push (push) Successful in 29s
Build and Deploy / deploy (push) Successful in 21s
2026-03-24 16:20:40 +01:00
1b7c0c48e7 Merge pull request 'dev' (#54) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
Build and Deploy / test-frontend (push) Successful in 59s
Build and Deploy / build-and-push (push) Successful in 28s
Build and Deploy / deploy (push) Successful in 19s
Reviewed-on: #54
2026-03-24 13:29:50 +01:00
printcalc-ci
cb86137730 style: apply prettier formatting
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 8s
PR Checks / security-sast (pull_request) Successful in 29s
PR Checks / test-backend (pull_request) Successful in 27s
PR Checks / test-frontend (pull_request) Successful in 1m2s
2026-03-24 12:19:19 +00:00
c8913af660 Merge remote-tracking branch 'origin/dev' into dev
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 13s
PR Checks / security-sast (pull_request) Successful in 31s
PR Checks / test-backend (pull_request) Successful in 28s
Build and Deploy / test-backend (push) Successful in 29s
PR Checks / test-frontend (pull_request) Successful in 1m5s
Build and Deploy / test-frontend (push) Successful in 1m1s
Build and Deploy / deploy (push) Successful in 21s
Build and Deploy / build-and-push (push) Successful in 31s
2026-03-24 13:17:30 +01:00
9611049e01 fix(front-end): new test 2026-03-24 13:17:25 +01:00
bad5947fb5 Merge branch 'main' into dev
Some checks failed
Build and Deploy / deploy (push) Has been cancelled
PR Checks / security-sast (pull_request) Successful in 32s
PR Checks / test-backend (pull_request) Successful in 31s
PR Checks / test-frontend (pull_request) Successful in 1m4s
Build and Deploy / test-backend (push) Has been cancelled
Build and Deploy / test-frontend (push) Has been cancelled
Build and Deploy / build-and-push (push) Has been cancelled
PR Checks / prettier-autofix (pull_request) Failing after 10s
2026-03-24 13:17:10 +01:00
d27558a3ee fix(front-end): fix no index product 3 hope the last one
Some checks failed
Build and Deploy / test-backend (push) Successful in 39s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 32s
Build and Deploy / deploy (push) Successful in 19s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / prettier-autofix (pull_request) Failing after 11s
PR Checks / test-backend (pull_request) Successful in 28s
PR Checks / test-frontend (pull_request) Successful in 1m3s
2026-03-24 12:59:09 +01:00
81f6f78c49 fix(front-end): fix no index product 2
All checks were successful
Build and Deploy / test-backend (push) Successful in 32s
Build and Deploy / test-frontend (push) Successful in 1m9s
Build and Deploy / build-and-push (push) Successful in 31s
Build and Deploy / deploy (push) Successful in 20s
2026-03-23 19:11:26 +01:00
bf593445bd fix(front-end): fix no index product
All checks were successful
Build and Deploy / test-backend (push) Successful in 35s
Build and Deploy / test-frontend (push) Successful in 1m4s
Build and Deploy / build-and-push (push) Successful in 1m16s
Build and Deploy / deploy (push) Successful in 20s
2026-03-23 18:07:07 +01:00
aa032c0140 Merge pull request 'fix(front-end): fix no index in products' (#53) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 29s
Build and Deploy / test-frontend (push) Successful in 1m6s
Build and Deploy / build-and-push (push) Successful in 28s
Build and Deploy / deploy (push) Successful in 20s
Reviewed-on: #53
2026-03-23 17:36:11 +01:00
printcalc-ci
95e60692c0 style: apply prettier formatting
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 8s
PR Checks / test-backend (pull_request) Successful in 29s
PR Checks / security-sast (pull_request) Successful in 37s
PR Checks / test-frontend (pull_request) Successful in 1m3s
2026-03-23 16:31:33 +00:00
fda2cdbecb Merge branch 'main' into dev
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 15s
PR Checks / security-sast (pull_request) Successful in 31s
PR Checks / test-backend (pull_request) Successful in 28s
Build and Deploy / build-and-push (push) Successful in 16s
Build and Deploy / test-backend (push) Successful in 29s
Build and Deploy / test-frontend (push) Successful in 1m5s
Build and Deploy / deploy (push) Successful in 23s
PR Checks / test-frontend (pull_request) Successful in 1m5s
2026-03-23 17:29:38 +01:00
a1cc9f18c4 fix(front-end): fix no index in products
Some checks failed
Build and Deploy / test-backend (push) Successful in 28s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 30s
Build and Deploy / deploy (push) Successful in 22s
PR Checks / prettier-autofix (pull_request) Failing after 11s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-frontend (pull_request) Successful in 1m4s
PR Checks / test-backend (pull_request) Successful in 27s
2026-03-23 17:27:18 +01:00
084d35d605 Merge pull request 'fix(front-end): seo improvemnts' (#52) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 28s
Build and Deploy / test-frontend (push) Successful in 59s
Build and Deploy / build-and-push (push) Successful in 30s
Build and Deploy / deploy (push) Successful in 22s
Reviewed-on: #52
2026-03-23 16:21:00 +01:00
printcalc-ci
02aac24a09 style: apply prettier formatting
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 8s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 27s
PR Checks / test-frontend (pull_request) Successful in 59s
2026-03-23 15:15:15 +00:00
51c2bf6985 Merge branch 'main' into dev
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 13s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 29s
Build and Deploy / test-backend (push) Successful in 27s
PR Checks / test-frontend (pull_request) Successful in 1m3s
Build and Deploy / test-frontend (push) Successful in 1m0s
Build and Deploy / build-and-push (push) Successful in 28s
Build and Deploy / deploy (push) Successful in 21s
2026-03-23 16:14:18 +01:00
4e99d12be1 fix(front-end): seo improvemnts
Some checks failed
Build and Deploy / build-and-push (push) Has been cancelled
Build and Deploy / deploy (push) Has been cancelled
Build and Deploy / test-backend (push) Has been cancelled
PR Checks / security-sast (pull_request) Successful in 32s
PR Checks / test-backend (pull_request) Successful in 28s
PR Checks / test-frontend (pull_request) Successful in 1m4s
PR Checks / prettier-autofix (pull_request) Failing after 12s
Build and Deploy / test-frontend (push) Has been cancelled
2026-03-23 16:14:04 +01:00
8b5d8f92e0 Merge pull request 'dev' (#51) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 35s
Build and Deploy / test-frontend (push) Successful in 1m7s
Build and Deploy / build-and-push (push) Successful in 16s
Build and Deploy / deploy (push) Successful in 19s
Reviewed-on: #51
2026-03-22 23:06:02 +01:00
d3c9dd6eb9 Merge branch 'main' into dev
All checks were successful
PR Checks / test-backend (pull_request) Successful in 30s
PR Checks / test-frontend (pull_request) Successful in 1m6s
PR Checks / prettier-autofix (pull_request) Successful in 8s
PR Checks / security-sast (pull_request) Successful in 33s
Build and Deploy / test-backend (push) Successful in 29s
Build and Deploy / test-frontend (push) Successful in 1m8s
Build and Deploy / build-and-push (push) Successful in 32s
Build and Deploy / deploy (push) Successful in 22s
2026-03-22 23:03:08 +01:00
254ff36c50 fix(front-end): seo improvemnts
Some checks failed
Build and Deploy / build-and-push (push) Has been cancelled
Build and Deploy / deploy (push) Has been cancelled
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 27s
Build and Deploy / test-frontend (push) Has been cancelled
Build and Deploy / test-backend (push) Has been cancelled
PR Checks / prettier-autofix (pull_request) Successful in 10s
PR Checks / test-frontend (pull_request) Successful in 1m6s
2026-03-22 23:02:59 +01:00
b317196217 fix(front-end): redirect
All checks were successful
Build and Deploy / test-backend (push) Successful in 40s
Build and Deploy / test-frontend (push) Successful in 1m5s
Build and Deploy / build-and-push (push) Successful in 1m19s
Build and Deploy / deploy (push) Successful in 22s
2026-03-22 22:41:12 +01:00
cc343ee27c fix(back-end): fix csrm and cors 2026-03-22 21:11:48 +01:00
74d1b16b7c fix(back-end): fix load product 2026-03-22 21:11:33 +01:00
adf6889712 Merge pull request 'dev' (#49) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 39s
Build and Deploy / test-frontend (push) Successful in 1m5s
Build and Deploy / build-and-push (push) Successful in 17s
Build and Deploy / deploy (push) Successful in 22s
Reviewed-on: #49
2026-03-21 18:57:57 +01:00
653082868a Merge pull request 'feat/brand-logo' (#50) from feat/brand-logo into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 35s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 27s
PR Checks / prettier-autofix (pull_request) Successful in 10s
Build and Deploy / test-frontend (push) Successful in 1m7s
PR Checks / test-frontend (pull_request) Successful in 1m5s
Build and Deploy / build-and-push (push) Successful in 1m21s
Build and Deploy / deploy (push) Successful in 19s
Reviewed-on: #50
2026-03-20 13:17:03 +01:00
997e770256 Merge remote-tracking branch 'origin/feat/brand-logo' into feat/brand-logo
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 11s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 28s
PR Checks / test-frontend (pull_request) Successful in 1m3s
2026-03-20 11:45:15 +01:00
fb1a6456e6 fix(back-end) base url fix 2026-03-20 11:45:10 +01:00
43cd80600e Merge branch 'dev' into feat/brand-logo
Some checks failed
PR Checks / prettier-autofix (pull_request) Successful in 11s
PR Checks / security-sast (pull_request) Successful in 32s
PR Checks / test-backend (pull_request) Failing after 27s
PR Checks / test-frontend (pull_request) Successful in 1m2s
2026-03-20 11:28:10 +01:00
printcalc-ci
23e1abdbbb style: apply prettier formatting 2026-03-20 09:37:56 +00:00
e575021f53 feat(front-end): new logo edited
Some checks failed
PR Checks / prettier-autofix (pull_request) Successful in 22s
PR Checks / security-sast (pull_request) Successful in 35s
PR Checks / test-backend (pull_request) Failing after 34s
PR Checks / test-frontend (pull_request) Successful in 1m1s
2026-03-20 10:36:50 +01:00
7e8c89ce45 feat(front-end): new logo edited 2026-03-19 14:33:29 +01:00
a40a8df894 feat(animation logo) 2026-03-18 17:30:53 +01:00
printcalc-ci
41f36ed18a style: apply prettier formatting 2026-03-17 08:03:30 +00:00
e04189bbfe Merge branch 'main' into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 28s
PR Checks / prettier-autofix (pull_request) Successful in 14s
Build and Deploy / test-frontend (push) Successful in 1m3s
PR Checks / security-sast (pull_request) Successful in 29s
PR Checks / test-backend (pull_request) Successful in 27s
PR Checks / test-frontend (pull_request) Successful in 1m6s
Build and Deploy / build-and-push (push) Successful in 1m46s
Build and Deploy / deploy (push) Successful in 22s
2026-03-17 09:01:34 +01:00
4ba408859d Merge pull request 'dev' (#48) from dev into main
All checks were successful
Build and Deploy / test-backend (push) Successful in 34s
Build and Deploy / test-frontend (push) Successful in 1m4s
Build and Deploy / build-and-push (push) Successful in 16s
Build and Deploy / deploy (push) Successful in 19s
Reviewed-on: #48
2026-03-14 19:18:15 +01:00
112 changed files with 17864 additions and 623 deletions

View File

@@ -0,0 +1,88 @@
package com.printcalculator.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@Service
public class AllowedOriginService {
private final List<String> allowedOrigins;
public AllowedOriginService(
@Value("${app.frontend.base-url:http://localhost:4200}") String frontendBaseUrl,
@Value("${app.cors.additional-allowed-origins:}") String additionalAllowedOrigins
) {
LinkedHashSet<String> configuredOrigins = new LinkedHashSet<>();
addConfiguredOrigin(configuredOrigins, frontendBaseUrl, "app.frontend.base-url");
for (String rawOrigin : additionalAllowedOrigins.split(",")) {
addConfiguredOrigin(configuredOrigins, rawOrigin, "app.cors.additional-allowed-origins");
}
if (configuredOrigins.isEmpty()) {
throw new IllegalStateException("At least one allowed origin must be configured.");
}
this.allowedOrigins = List.copyOf(configuredOrigins);
}
public List<String> getAllowedOrigins() {
return allowedOrigins;
}
public boolean isAllowed(String rawOriginOrUrl) {
String normalizedOrigin = normalizeRequestOrigin(rawOriginOrUrl);
return normalizedOrigin != null && allowedOrigins.contains(normalizedOrigin);
}
private void addConfiguredOrigin(Set<String> configuredOrigins, String rawOriginOrUrl, String propertyName) {
if (rawOriginOrUrl == null || rawOriginOrUrl.isBlank()) {
return;
}
String normalizedOrigin = normalizeRequestOrigin(rawOriginOrUrl);
if (normalizedOrigin == null) {
throw new IllegalStateException(propertyName + " must contain absolute http(s) URLs.");
}
configuredOrigins.add(normalizedOrigin);
}
private String normalizeRequestOrigin(String rawOriginOrUrl) {
if (rawOriginOrUrl == null || rawOriginOrUrl.isBlank()) {
return null;
}
try {
URI uri = URI.create(rawOriginOrUrl.trim());
String scheme = uri.getScheme();
String host = uri.getHost();
if (scheme == null || host == null) {
return null;
}
String normalizedScheme = scheme.toLowerCase(Locale.ROOT);
if (!"http".equals(normalizedScheme) && !"https".equals(normalizedScheme)) {
return null;
}
String normalizedHost = host.toLowerCase(Locale.ROOT);
int port = uri.getPort();
if (isDefaultPort(normalizedScheme, port) || port < 0) {
return normalizedScheme + "://" + normalizedHost;
}
return normalizedScheme + "://" + normalizedHost + ":" + port;
} catch (IllegalArgumentException ignored) {
return null;
}
}
private boolean isDefaultPort(String scheme, int port) {
return ("http".equals(scheme) && port == 80)
|| ("https".equals(scheme) && port == 443);
}
}

View File

@@ -1,27 +1,27 @@
package com.printcalculator.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
public class CorsConfig {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(
"http://localhost",
"http://localhost:4200",
"http://localhost:80",
"http://127.0.0.1",
"https://dev.3d-fab.ch",
"https://int.3d-fab.ch",
"https://3d-fab.ch"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
.allowedHeaders("*")
.allowCredentials(true);
@Bean
public CorsConfigurationSource corsConfigurationSource(AllowedOriginService allowedOriginService) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(allowedOriginService.getAllowedOrigins());
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

View File

@@ -1,5 +1,6 @@
package com.printcalculator.config;
import com.printcalculator.security.AdminCsrfProtectionFilter;
import com.printcalculator.security.AdminSessionAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -18,6 +19,7 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
AdminCsrfProtectionFilter adminCsrfProtectionFilter,
AdminSessionAuthenticationFilter adminSessionAuthenticationFilter
) throws Exception {
http
@@ -40,7 +42,8 @@ public class SecurityConfig {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{\"error\":\"UNAUTHORIZED\"}");
}))
.addFilterBefore(adminSessionAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(adminCsrfProtectionFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(adminSessionAuthenticationFilter, AdminCsrfProtectionFilter.class);
return http.build();
}

View File

@@ -56,6 +56,18 @@ public class PublicShopController {
return ResponseEntity.ok(publicShopCatalogService.getProduct(slug, lang));
}
@GetMapping("/products/by-path/{publicPath}")
public ResponseEntity<ShopProductDetailDto> getProductByPublicPath(@PathVariable String publicPath,
@RequestParam(required = false) String lang) {
return ResponseEntity.ok(publicShopCatalogService.getProductByPublicPath(publicPath, lang));
}
@GetMapping("/products/by-id-prefix/{idPrefix}")
public ResponseEntity<ShopProductDetailDto> getProductByIdPrefix(@PathVariable String idPrefix,
@RequestParam(required = false) String lang) {
return ResponseEntity.ok(publicShopCatalogService.getProductByIdPrefix(idPrefix, lang));
}
@GetMapping("/products/{slug}/model")
public ResponseEntity<Resource> getProductModel(@PathVariable String slug) throws IOException {
PublicShopCatalogService.ProductModelDownload model = publicShopCatalogService.getProductModelDownload(slug);

View File

@@ -18,6 +18,7 @@ import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
@@ -124,6 +125,9 @@ public class QuoteController {
if (file.isEmpty()) {
return ResponseEntity.badRequest().build();
}
if (!isSupportedInputFile(file)) {
throw new ResponseStatusException(BAD_REQUEST, "Unsupported file type. Allowed: stl, 3mf");
}
// Scan for virus
clamAVService.scan(file.getInputStream());
@@ -153,4 +157,14 @@ public class QuoteController {
Files.deleteIfExists(tempInput);
}
}
private boolean isSupportedInputFile(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.isBlank()) {
return false;
}
String normalized = originalFilename.toLowerCase(Locale.ROOT);
return normalized.endsWith(".stl") || normalized.endsWith(".3mf");
}
}

View File

@@ -2,6 +2,7 @@ package com.printcalculator.dto;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public record ShopProductDetailDto(
@@ -25,6 +26,8 @@ public record ShopProductDetailDto(
List<ShopProductVariantOptionDto> variants,
PublicMediaUsageDto primaryImage,
List<PublicMediaUsageDto> images,
ShopProductModelDto model3d
ShopProductModelDto model3d,
String publicPath,
Map<String, String> localizedPaths
) {
}

View File

@@ -1,6 +1,7 @@
package com.printcalculator.dto;
import java.math.BigDecimal;
import java.util.Map;
import java.util.UUID;
public record ShopProductSummaryDto(
@@ -15,6 +16,8 @@ public record ShopProductSummaryDto(
BigDecimal priceToChf,
ShopProductVariantOptionDto defaultVariant,
PublicMediaUsageDto primaryImage,
ShopProductModelDto model3d
ShopProductModelDto model3d,
String publicPath,
Map<String, String> localizedPaths
) {
}

View File

@@ -223,10 +223,15 @@ public class OrderEmailListener {
order.getCreatedAt().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(locale))
);
templateData.put("totalCost", currencyFormatter.format(order.getTotalChf()));
templateData.put("logoUrl", buildLogoUrl());
templateData.put("currentYear", Year.now().getValue());
return templateData;
}
private String buildLogoUrl() {
return frontendBaseUrl.replaceAll("/+$", "") + "/assets/images/brand-logo-yellow.svg";
}
private String applyOrderConfirmationTexts(Map<String, Object> templateData, String language, String orderNumber) {
return switch (language) {
case "en" -> {

View File

@@ -0,0 +1,60 @@
package com.printcalculator.security;
import com.printcalculator.config.AllowedOriginService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Locale;
import java.util.Set;
@Component
public class AdminCsrfProtectionFilter extends OncePerRequestFilter {
private static final Set<String> SAFE_METHODS = Set.of("GET", "HEAD", "OPTIONS", "TRACE");
private final AllowedOriginService allowedOriginService;
public AdminCsrfProtectionFilter(AllowedOriginService allowedOriginService) {
this.allowedOriginService = allowedOriginService;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = resolvePath(request);
String method = request.getMethod() == null ? "" : request.getMethod().toUpperCase(Locale.ROOT);
return !path.startsWith("/api/admin/") || SAFE_METHODS.contains(method);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String origin = request.getHeader(HttpHeaders.ORIGIN);
String referer = request.getHeader(HttpHeaders.REFERER);
if (allowedOriginService.isAllowed(origin) || allowedOriginService.isAllowed(referer)) {
filterChain.doFilter(request, response);
return;
}
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{\"error\":\"CSRF_INVALID\"}");
}
private String resolvePath(HttpServletRequest request) {
String path = request.getRequestURI();
String contextPath = request.getContextPath();
if (contextPath != null && !contextPath.isEmpty() && path.startsWith(contextPath)) {
return path.substring(contextPath.length());
}
return path;
}
}

View File

@@ -72,10 +72,14 @@ public class QuoteSessionItemService {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot modify a converted session");
}
String ext = quoteStorageService.getSafeExtension(file.getOriginalFilename(), "");
if (ext.isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unsupported file type. Allowed: stl, 3mf");
}
clamAVService.scan(file.getInputStream());
Path sessionStorageDir = quoteStorageService.sessionStorageDir(session.getId());
String ext = quoteStorageService.getSafeExtension(file.getOriginalFilename(), "stl");
String storedFilename = UUID.randomUUID() + "." + ext;
Path persistentPath = quoteStorageService.resolveSessionPath(sessionStorageDir, storedFilename);

View File

@@ -54,7 +54,6 @@ public class QuoteStorageService {
return switch (ext) {
case "stl" -> "stl";
case "3mf" -> "3mf";
case "step", "stp" -> "step";
default -> fallback;
};
}

View File

@@ -30,6 +30,9 @@ public class CustomQuoteRequestNotificationService {
@Value("${app.mail.contact-request.customer.enabled:true}")
private boolean contactRequestCustomerMailEnabled;
@Value("${app.frontend.base-url:http://localhost:4200}")
private String frontendBaseUrl;
public CustomQuoteRequestNotificationService(EmailNotificationService emailNotificationService,
ContactRequestLocalizationService localizationService) {
this.emailNotificationService = emailNotificationService;
@@ -63,6 +66,7 @@ public class CustomQuoteRequestNotificationService {
templateData.put("phone", safeValue(request.getPhone()));
templateData.put("message", safeValue(request.getMessage()));
templateData.put("attachmentsCount", attachmentsCount);
templateData.put("logoUrl", buildLogoUrl());
templateData.put("currentYear", Year.now().getValue());
emailNotificationService.sendEmail(
@@ -101,6 +105,7 @@ public class CustomQuoteRequestNotificationService {
templateData.put("phone", safeValue(request.getPhone()));
templateData.put("message", safeValue(request.getMessage()));
templateData.put("attachmentsCount", attachmentsCount);
templateData.put("logoUrl", buildLogoUrl());
templateData.put("currentYear", Year.now().getValue());
String subject = localizationService.applyCustomerContactRequestTexts(templateData, language, request.getId());
@@ -119,4 +124,11 @@ public class CustomQuoteRequestNotificationService {
}
return value;
}
private String buildLogoUrl() {
String baseUrl = frontendBaseUrl == null || frontendBaseUrl.isBlank()
? "http://localhost:4200"
: frontendBaseUrl;
return baseUrl.replaceAll("/+$", "") + "/assets/images/brand-logo-yellow.svg";
}
}

View File

@@ -126,24 +126,62 @@ public class PublicShopCatalogService {
}
public ShopProductDetailDto getProduct(String slug, String language) {
CategoryContext categoryContext = loadCategoryContext(language);
PublicProductContext productContext = loadPublicProductContext(categoryContext, language);
String normalizedLanguage = normalizeLanguage(language);
CategoryContext categoryContext = loadCategoryContext(normalizedLanguage);
PublicProductContext productContext = loadPublicProductContext(categoryContext, normalizedLanguage);
ProductEntry entry = requirePublicProductEntry(
productContext.entriesBySlug().get(slug),
categoryContext
);
return toProductDetailDto(
entry,
productContext.productMediaBySlug(),
productContext.variantColorHexByMaterialAndColor(),
normalizedLanguage
);
}
ProductEntry entry = productContext.entriesBySlug().get(slug);
if (entry == null) {
public ShopProductDetailDto getProductByPublicPath(String publicPathSegment, String language) {
String normalizedLanguage = normalizeLanguage(language);
String normalizedPublicPath = normalizePublicPathSegment(publicPathSegment);
if (normalizedPublicPath == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
}
ShopCategory category = entry.product().getCategory();
if (category == null || !categoryContext.categoriesById().containsKey(category.getId())) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
}
CategoryContext categoryContext = loadCategoryContext(normalizedLanguage);
PublicProductContext productContext = loadPublicProductContext(categoryContext, normalizedLanguage);
ProductEntry entry = requirePublicProductEntry(
productContext.entriesByPublicPath().get(normalizedPublicPath),
categoryContext
);
return toProductDetailDto(
entry,
productContext.productMediaBySlug(),
productContext.variantColorHexByMaterialAndColor(),
language
normalizedLanguage
);
}
public ShopProductDetailDto getProductByIdPrefix(String idPrefix, String language) {
String normalizedLanguage = normalizeLanguage(language);
String normalizedIdPrefix = normalizeProductIdPrefix(idPrefix);
if (normalizedIdPrefix == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
}
CategoryContext categoryContext = loadCategoryContext(normalizedLanguage);
PublicProductContext productContext = loadPublicProductContext(categoryContext, normalizedLanguage);
ProductEntry entry = requirePublicProductEntry(
productContext.entriesByIdPrefix().get(normalizedIdPrefix),
categoryContext
);
return toProductDetailDto(
entry,
productContext.productMediaBySlug(),
productContext.variantColorHexByMaterialAndColor(),
normalizedLanguage
);
}
@@ -197,6 +235,7 @@ public class PublicShopCatalogService {
}
private PublicProductContext loadPublicProductContext(CategoryContext categoryContext, String language) {
String normalizedLanguage = normalizeLanguage(language);
List<ProductEntry> entries = loadPublicProducts(categoryContext.categoriesById().keySet());
Map<String, List<PublicMediaUsageDto>> productMediaBySlug = publicMediaQueryService.getUsageMediaMap(
SHOP_PRODUCT_MEDIA_USAGE_TYPE,
@@ -207,8 +246,29 @@ public class PublicShopCatalogService {
Map<String, ProductEntry> entriesBySlug = entries.stream()
.collect(Collectors.toMap(entry -> entry.product().getSlug(), entry -> entry, (left, right) -> left, LinkedHashMap::new));
Map<String, ProductEntry> entriesByPublicPath = entries.stream()
.collect(Collectors.toMap(
entry -> normalizePublicPathSegment(ShopPublicPathSupport.buildProductPathSegment(entry.product(), normalizedLanguage)),
entry -> entry,
(left, right) -> left,
LinkedHashMap::new
));
Map<String, ProductEntry> entriesByIdPrefix = entries.stream()
.collect(Collectors.toMap(
entry -> normalizeProductIdPrefix(ShopPublicPathSupport.productIdPrefix(entry.product().getId())),
entry -> entry,
(left, right) -> left,
LinkedHashMap::new
));
return new PublicProductContext(entries, entriesBySlug, productMediaBySlug, variantColorHexByMaterialAndColor);
return new PublicProductContext(
entries,
entriesBySlug,
entriesByPublicPath,
entriesByIdPrefix,
productMediaBySlug,
variantColorHexByMaterialAndColor
);
}
private Map<String, String> buildFilamentVariantColorHexMap() {
@@ -399,6 +459,9 @@ public class PublicShopCatalogService {
Map<String, String> variantColorHexByMaterialAndColor,
String language) {
List<PublicMediaUsageDto> images = productMediaBySlug.getOrDefault(productMediaUsageKey(entry.product()), List.of());
String normalizedLanguage = normalizeLanguage(language);
String publicPathSegment = ShopPublicPathSupport.buildProductPathSegment(entry.product(), normalizedLanguage);
Map<String, String> localizedPaths = ShopPublicPathSupport.buildLocalizedProductPaths(entry.product());
return new ShopProductSummaryDto(
entry.product().getId(),
entry.product().getSlug(),
@@ -415,7 +478,9 @@ public class PublicShopCatalogService {
resolvePriceTo(entry.variants()),
toVariantDto(entry.defaultVariant(), entry.defaultVariant(), variantColorHexByMaterialAndColor, language),
selectPrimaryMedia(images),
toProductModelDto(entry)
toProductModelDto(entry),
publicPathSegment,
localizedPaths
);
}
@@ -426,8 +491,10 @@ public class PublicShopCatalogService {
List<PublicMediaUsageDto> images = productMediaBySlug.getOrDefault(productMediaUsageKey(entry.product()), List.of());
String localizedSeoTitle = entry.product().getSeoTitleForLanguage(language);
String localizedSeoDescription = entry.product().getSeoDescriptionForLanguage(language);
return new ShopProductDetailDto(
entry.product().getId(),
String normalizedLanguage = normalizeLanguage(language);
String publicPathSegment = ShopPublicPathSupport.buildProductPathSegment(entry.product(), normalizedLanguage);
Map<String, String> localizedPaths = ShopPublicPathSupport.buildLocalizedProductPaths(entry.product());
return new ShopProductDetailDto(entry.product().getId(),
entry.product().getSlug(),
entry.product().getNameForLanguage(language),
entry.product().getExcerptForLanguage(language),
@@ -453,7 +520,9 @@ public class PublicShopCatalogService {
.toList(),
selectPrimaryMedia(images),
images,
toProductModelDto(entry)
toProductModelDto(entry),
publicPathSegment,
localizedPaths
);
}
@@ -506,6 +575,36 @@ public class PublicShopCatalogService {
return raw.toLowerCase(Locale.ROOT);
}
private ProductEntry requirePublicProductEntry(ProductEntry entry, CategoryContext categoryContext) {
if (entry == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
}
ShopCategory category = entry.product().getCategory();
if (category == null || !categoryContext.categoriesById().containsKey(category.getId())) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
}
return entry;
}
private String normalizePublicPathSegment(String publicPathSegment) {
String normalized = trimToNull(publicPathSegment);
if (normalized == null) {
return null;
}
return normalized.toLowerCase(Locale.ROOT);
}
private String normalizeProductIdPrefix(String idPrefix) {
String normalized = trimToNull(idPrefix);
if (normalized == null) {
return null;
}
normalized = normalized.toLowerCase(Locale.ROOT);
return normalized.matches("^[0-9a-f]{8}$") ? normalized : null;
}
private String trimToNull(String value) {
String raw = String.valueOf(value == null ? "" : value).trim();
if (raw.isEmpty()) {
@@ -514,6 +613,22 @@ public class PublicShopCatalogService {
return raw;
}
private String normalizeLanguage(String language) {
String normalized = trimToNull(language);
if (normalized == null) {
return "it";
}
normalized = normalized.toLowerCase(Locale.ROOT);
int separatorIndex = normalized.indexOf('-');
if (separatorIndex > 0) {
normalized = normalized.substring(0, separatorIndex);
}
return switch (normalized) {
case "en", "de", "fr" -> normalized;
default -> "it";
};
}
private ShopProductModelDto toProductModelDto(ProductEntry entry) {
if (entry.modelAsset() == null) {
return null;
@@ -585,6 +700,8 @@ public class PublicShopCatalogService {
private record PublicProductContext(
List<ProductEntry> entries,
Map<String, ProductEntry> entriesBySlug,
Map<String, ProductEntry> entriesByPublicPath,
Map<String, ProductEntry> entriesByIdPrefix,
Map<String, List<PublicMediaUsageDto>> productMediaBySlug,
Map<String, String> variantColorHexByMaterialAndColor
) {

View File

@@ -0,0 +1,66 @@
package com.printcalculator.service.shop;
import com.printcalculator.entity.ShopProduct;
import java.text.Normalizer;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
final class ShopPublicPathSupport {
private static final String PRODUCT_ROUTE_PREFIX = "/shop/p/";
private ShopPublicPathSupport() {
}
static String buildProductPathSegment(ShopProduct product, String language) {
String localizedName = product.getNameForLanguage(language);
String idPrefix = productIdPrefix(product.getId());
String tail = firstNonBlank(slugify(localizedName), slugify(product.getSlug()), "product");
return idPrefix.isBlank() ? tail : idPrefix + "-" + tail;
}
static Map<String, String> buildLocalizedProductPaths(ShopProduct product) {
Map<String, String> localizedPaths = new LinkedHashMap<>();
for (String language : ShopProduct.SUPPORTED_LANGUAGES) {
localizedPaths.put(language, "/" + language + PRODUCT_ROUTE_PREFIX + buildProductPathSegment(product, language));
}
return localizedPaths;
}
static String productIdPrefix(UUID productId) {
if (productId == null) {
return "";
}
String raw = productId.toString().trim().toLowerCase(Locale.ROOT);
int dashIndex = raw.indexOf('-');
if (dashIndex > 0) {
return raw.substring(0, dashIndex);
}
return raw.length() >= 8 ? raw.substring(0, 8) : raw;
}
static String slugify(String rawValue) {
String safeValue = rawValue == null ? "" : rawValue;
String normalized = Normalizer.normalize(safeValue, Normalizer.Form.NFD)
.replaceAll("\\p{M}+", "")
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-+|-+$", "")
.replaceAll("-{2,}", "-");
return normalized;
}
private static String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return null;
}
}

View File

@@ -11,7 +11,6 @@ import org.springframework.transaction.annotation.Transactional;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
@@ -19,7 +18,6 @@ import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -31,6 +29,12 @@ public class ShopSitemapService {
private static final List<String> SUPPORTED_LANGUAGES = ShopProduct.SUPPORTED_LANGUAGES;
private static final String DEFAULT_LANGUAGE = "it";
private static final DateTimeFormatter LASTMOD_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
private static final Map<String, String> HREFLANG_BY_LANGUAGE = Map.of(
"it", "it-CH",
"en", "en-CH",
"de", "de-CH",
"fr", "fr-CH"
);
private final ShopCategoryRepository shopCategoryRepository;
private final ShopProductRepository shopProductRepository;
@@ -130,7 +134,7 @@ public class ShopSitemapService {
Map<String, String> hrefByLanguage = new LinkedHashMap<>();
for (String language : SUPPORTED_LANGUAGES) {
String publicSegment = localizedProductPathSegment(product, language);
String publicSegment = ShopPublicPathSupport.buildProductPathSegment(product, language);
hrefByLanguage.put(language, frontendBaseUrl + "/" + language + "/shop/p/" + pathEncodeSegment(publicSegment));
}
@@ -169,7 +173,7 @@ public class ShopSitemapService {
continue;
}
xml.append(" <xhtml:link rel=\"alternate\" hreflang=\"")
.append(language)
.append(HREFLANG_BY_LANGUAGE.getOrDefault(language, language))
.append("\" href=\"")
.append(xmlEscape(href))
.append("\" />\n");
@@ -186,48 +190,6 @@ public class ShopSitemapService {
xml.append(" </url>\n");
}
private String localizedProductPathSegment(ShopProduct product, String language) {
String localizedName = product.getNameForLanguage(language);
String idPrefix = productIdPrefix(product.getId());
String tail = firstNonBlank(slugify(localizedName), slugify(product.getSlug()), "product");
return idPrefix.isBlank() ? tail : idPrefix + "-" + tail;
}
private String productIdPrefix(UUID productId) {
if (productId == null) {
return "";
}
String raw = productId.toString().trim().toLowerCase(Locale.ROOT);
int dashIndex = raw.indexOf('-');
if (dashIndex > 0) {
return raw.substring(0, dashIndex);
}
return raw.length() >= 8 ? raw.substring(0, 8) : raw;
}
static String slugify(String rawValue) {
String safeValue = rawValue == null ? "" : rawValue;
String normalized = Normalizer.normalize(safeValue, Normalizer.Form.NFD)
.replaceAll("\\p{M}+", "")
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-+|-+$", "")
.replaceAll("-{2,}", "-");
return normalized;
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return null;
}
private String pathEncodeSegment(String rawSegment) {
String safeSegment = rawSegment == null ? "" : rawSegment;
return URLEncoder.encode(safeSegment, StandardCharsets.UTF_8).replace("+", "%20");

View File

@@ -56,6 +56,7 @@ app.mail.contact-request.admin.enabled=${APP_MAIL_CONTACT_REQUEST_ADMIN_ENABLED:
app.mail.contact-request.admin.address=${APP_MAIL_CONTACT_REQUEST_ADMIN_ADDRESS:info@3d-fab.ch}
app.mail.contact-request.customer.enabled=${APP_MAIL_CONTACT_REQUEST_CUSTOMER_ENABLED:true}
app.frontend.base-url=${APP_FRONTEND_BASE_URL:http://localhost:4200}
app.cors.additional-allowed-origins=${APP_CORS_ADDITIONAL_ALLOWED_ORIGINS:}
app.sitemap.shop.cache-seconds=${APP_SITEMAP_SHOP_CACHE_SECONDS:3600}
openai.translation.api-key=${OPENAI_API_KEY:}
openai.translation.base-url=${OPENAI_BASE_URL:https://api.openai.com/v1}

View File

@@ -25,6 +25,21 @@
color: #222222;
}
.header {
text-align: center;
border-bottom: 1px solid #eeeeee;
padding-bottom: 20px;
margin-bottom: 20px;
}
.brand-logo {
display: block;
width: 220px;
max-width: 220px;
height: auto;
margin: 0 auto 16px;
}
p {
color: #444444;
line-height: 1.5;
@@ -63,7 +78,10 @@
</head>
<body>
<div class="container">
<h1>Nuova richiesta di contatto</h1>
<div class="header">
<img class="brand-logo" th:src="${logoUrl}" src="https://example.com/assets/images/brand-logo-yellow.svg" alt="3D-Fab">
<h1>Nuova richiesta di contatto</h1>
</div>
<p>E' stata ricevuta una nuova richiesta dal form contatti/su misura.</p>
<table>

View File

@@ -25,6 +25,21 @@
color: #222222;
}
.header {
text-align: center;
border-bottom: 1px solid #eeeeee;
padding-bottom: 20px;
margin-bottom: 20px;
}
.brand-logo {
display: block;
width: 220px;
max-width: 220px;
height: auto;
margin: 0 auto 16px;
}
h2 {
margin-top: 18px;
color: #222222;
@@ -69,7 +84,10 @@
</head>
<body>
<div class="container">
<h1 th:text="${headlineText}">We received your contact request</h1>
<div class="header">
<img class="brand-logo" th:src="${logoUrl}" src="https://example.com/assets/images/brand-logo-yellow.svg" alt="3D-Fab">
<h1 th:text="${headlineText}">We received your contact request</h1>
</div>
<p th:text="${greetingText}">Hi customer,</p>
<p th:text="${introText}">Thank you for contacting us. Our team will reply as soon as possible.</p>
<p>

View File

@@ -27,8 +27,17 @@
margin-bottom: 20px;
}
.brand-logo {
display: block;
width: 220px;
max-width: 220px;
height: auto;
margin: 0 auto 16px;
}
.header h1 {
color: #333333;
margin: 0;
}
.content {
@@ -67,6 +76,7 @@
<body>
<div class="container">
<div class="header">
<img class="brand-logo" th:src="${logoUrl}" src="https://example.com/assets/images/brand-logo-yellow.svg" alt="3D-Fab">
<h1 th:text="${headlineText}">Thank you for your order #00000000</h1>
</div>

View File

@@ -27,8 +27,17 @@
margin-bottom: 20px;
}
.brand-logo {
display: block;
width: 220px;
max-width: 220px;
height: auto;
margin: 0 auto 16px;
}
.header h1 {
color: #333333;
margin: 0;
}
.content {
@@ -70,6 +79,7 @@
<body>
<div class="container">
<div class="header">
<img class="brand-logo" th:src="${logoUrl}" src="https://example.com/assets/images/brand-logo-yellow.svg" alt="3D-Fab">
<h1 th:text="${headlineText}">Your order #00000000 has been shipped</h1>
</div>

View File

@@ -27,8 +27,17 @@
margin-bottom: 20px;
}
.brand-logo {
display: block;
width: 220px;
max-width: 220px;
height: auto;
margin: 0 auto 16px;
}
.header h1 {
color: #333333;
margin: 0;
}
.content {
@@ -70,6 +79,7 @@
<body>
<div class="container">
<div class="header">
<img class="brand-logo" th:src="${logoUrl}" src="https://example.com/assets/images/brand-logo-yellow.svg" alt="3D-Fab">
<h1 th:text="${headlineText}">Payment confirmed for order #00000000</h1>
</div>

View File

@@ -27,8 +27,17 @@
margin-bottom: 20px;
}
.brand-logo {
display: block;
width: 220px;
max-width: 220px;
height: auto;
margin: 0 auto 16px;
}
.header h1 {
color: #333333;
margin: 0;
}
.content {
@@ -70,6 +79,7 @@
<body>
<div class="container">
<div class="header">
<img class="brand-logo" th:src="${logoUrl}" src="https://example.com/assets/images/brand-logo-yellow.svg" alt="3D-Fab">
<h1 th:text="${headlineText}">Payment reported for order #00000000</h1>
</div>

View File

@@ -1,7 +1,10 @@
package com.printcalculator.controller;
import com.printcalculator.config.AllowedOriginService;
import com.printcalculator.config.CorsConfig;
import com.printcalculator.config.SecurityConfig;
import com.printcalculator.controller.admin.AdminAuthController;
import com.printcalculator.security.AdminCsrfProtectionFilter;
import com.printcalculator.security.AdminLoginThrottleService;
import com.printcalculator.security.AdminSessionAuthenticationFilter;
import com.printcalculator.security.AdminSessionService;
@@ -19,13 +22,18 @@ import org.springframework.test.web.servlet.MvcResult;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(controllers = AdminAuthController.class)
@Import({
CorsConfig.class,
AllowedOriginService.class,
SecurityConfig.class,
AdminCsrfProtectionFilter.class,
AdminSessionAuthenticationFilter.class,
AdminSessionService.class,
AdminLoginThrottleService.class
@@ -37,6 +45,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
})
class AdminAuthSecurityTest {
private static final String ALLOWED_ORIGIN = "http://localhost:4200";
@Autowired
private MockMvc mockMvc;
@@ -47,6 +57,7 @@ class AdminAuthSecurityTest {
req.setRemoteAddr("10.0.0.1");
return req;
})
.header(HttpHeaders.ORIGIN, ALLOWED_ORIGIN)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"password\":\"test-admin-password\"}"))
.andExpect(status().isOk())
@@ -69,6 +80,7 @@ class AdminAuthSecurityTest {
req.setRemoteAddr("10.0.0.2");
return req;
})
.header(HttpHeaders.ORIGIN, ALLOWED_ORIGIN)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"password\":\"wrong-password\"}"))
.andExpect(status().isUnauthorized())
@@ -83,6 +95,7 @@ class AdminAuthSecurityTest {
req.setRemoteAddr("10.0.0.3");
return req;
})
.header(HttpHeaders.ORIGIN, ALLOWED_ORIGIN)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"password\":\"wrong-password\"}"))
.andExpect(status().isUnauthorized())
@@ -93,12 +106,36 @@ class AdminAuthSecurityTest {
req.setRemoteAddr("10.0.0.3");
return req;
})
.header(HttpHeaders.ORIGIN, ALLOWED_ORIGIN)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"password\":\"wrong-password\"}"))
.andExpect(status().isTooManyRequests())
.andExpect(jsonPath("$.authenticated").value(false));
}
@Test
void loginWithoutTrustedOrigin_ShouldReturnForbidden() throws Exception {
mockMvc.perform(post("/api/admin/auth/login")
.with(req -> {
req.setRemoteAddr("10.0.0.30");
return req;
})
.contentType(MediaType.APPLICATION_JSON)
.content("{\"password\":\"test-admin-password\"}"))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.error").value("CSRF_INVALID"));
}
@Test
void preflightFromAllowedOrigin_ShouldExposeCorsHeaders() throws Exception {
mockMvc.perform(options("/api/admin/auth/login")
.header(HttpHeaders.ORIGIN, ALLOWED_ORIGIN)
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, ALLOWED_ORIGIN))
.andExpect(header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"));
}
@Test
void adminAccessWithoutCookie_ShouldReturn401() throws Exception {
mockMvc.perform(get("/api/admin/auth/me"))
@@ -112,6 +149,7 @@ class AdminAuthSecurityTest {
req.setRemoteAddr("10.0.0.4");
return req;
})
.header(HttpHeaders.ORIGIN, ALLOWED_ORIGIN)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"password\":\"test-admin-password\"}"))
.andExpect(status().isOk())

View File

@@ -1,7 +1,10 @@
package com.printcalculator.controller.admin;
import com.printcalculator.config.AllowedOriginService;
import com.printcalculator.config.CorsConfig;
import com.printcalculator.config.SecurityConfig;
import com.printcalculator.service.order.AdminOrderControllerService;
import com.printcalculator.security.AdminCsrfProtectionFilter;
import com.printcalculator.security.AdminLoginThrottleService;
import com.printcalculator.security.AdminSessionAuthenticationFilter;
import com.printcalculator.security.AdminSessionService;
@@ -35,7 +38,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@WebMvcTest(controllers = {AdminAuthController.class, AdminOrderController.class})
@Import({
CorsConfig.class,
AllowedOriginService.class,
SecurityConfig.class,
AdminCsrfProtectionFilter.class,
AdminSessionAuthenticationFilter.class,
AdminSessionService.class,
AdminLoginThrottleService.class,
@@ -48,6 +54,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
})
class AdminOrderControllerSecurityTest {
private static final String ALLOWED_ORIGIN = "http://localhost:4200";
@Autowired
private MockMvc mockMvc;
@@ -96,6 +104,7 @@ class AdminOrderControllerSecurityTest {
req.setRemoteAddr("10.0.0.44");
return req;
})
.header(HttpHeaders.ORIGIN, ALLOWED_ORIGIN)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"password\":\"test-admin-password\"}"))
.andExpect(status().isOk())

View File

@@ -1,9 +1,12 @@
package com.printcalculator.controller.admin;
import com.printcalculator.config.AllowedOriginService;
import com.printcalculator.config.CorsConfig;
import com.printcalculator.config.SecurityConfig;
import com.printcalculator.dto.AdminTranslateShopProductResponse;
import com.printcalculator.service.admin.AdminShopProductControllerService;
import com.printcalculator.service.admin.AdminShopProductTranslationService;
import com.printcalculator.security.AdminCsrfProtectionFilter;
import com.printcalculator.security.AdminLoginThrottleService;
import com.printcalculator.security.AdminSessionAuthenticationFilter;
import com.printcalculator.security.AdminSessionService;
@@ -36,7 +39,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@WebMvcTest(controllers = {AdminAuthController.class, AdminShopProductController.class})
@Import({
CorsConfig.class,
AllowedOriginService.class,
SecurityConfig.class,
AdminCsrfProtectionFilter.class,
AdminSessionAuthenticationFilter.class,
AdminSessionService.class,
AdminLoginThrottleService.class,
@@ -49,6 +55,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
})
class AdminShopProductControllerSecurityTest {
private static final String ALLOWED_ORIGIN = "http://localhost:4200";
@Autowired
private MockMvc mockMvc;
@@ -61,11 +69,22 @@ class AdminShopProductControllerSecurityTest {
@Test
void translateProduct_withoutAdminCookie_shouldReturn401() throws Exception {
mockMvc.perform(post("/api/admin/shop/products/translate")
.header(HttpHeaders.ORIGIN, ALLOWED_ORIGIN)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"sourceLanguage\":\"it\",\"names\":{\"it\":\"Supporto cavo\"}}"))
.andExpect(status().isUnauthorized());
}
@Test
void translateProduct_withAdminCookieAndMissingOrigin_shouldReturn403() throws Exception {
mockMvc.perform(post("/api/admin/shop/products/translate")
.cookie(loginAndExtractCookie())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"sourceLanguage\":\"it\",\"names\":{\"it\":\"Supporto cavo\"}}"))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.error").value("CSRF_INVALID"));
}
@Test
void translateProduct_withAdminCookie_shouldReturnTranslations() throws Exception {
AdminTranslateShopProductResponse response = new AdminTranslateShopProductResponse();
@@ -82,6 +101,7 @@ class AdminShopProductControllerSecurityTest {
mockMvc.perform(post("/api/admin/shop/products/translate")
.cookie(loginAndExtractCookie())
.header(HttpHeaders.ORIGIN, ALLOWED_ORIGIN)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
@@ -107,6 +127,7 @@ class AdminShopProductControllerSecurityTest {
req.setRemoteAddr("10.0.0.44");
return req;
})
.header(HttpHeaders.ORIGIN, ALLOWED_ORIGIN)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"password\":\"test-admin-password\"}"))
.andExpect(status().isOk())

View File

@@ -0,0 +1,191 @@
package com.printcalculator.service.shop;
import com.printcalculator.dto.ShopProductCatalogResponseDto;
import com.printcalculator.dto.ShopProductDetailDto;
import com.printcalculator.entity.ShopCategory;
import com.printcalculator.entity.ShopProduct;
import com.printcalculator.entity.ShopProductVariant;
import com.printcalculator.repository.FilamentVariantRepository;
import com.printcalculator.repository.ShopCategoryRepository;
import com.printcalculator.repository.ShopProductModelAssetRepository;
import com.printcalculator.repository.ShopProductRepository;
import com.printcalculator.repository.ShopProductVariantRepository;
import com.printcalculator.service.media.PublicMediaQueryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PublicShopCatalogServiceTest {
@Mock
private ShopCategoryRepository shopCategoryRepository;
@Mock
private ShopProductRepository shopProductRepository;
@Mock
private ShopProductVariantRepository shopProductVariantRepository;
@Mock
private ShopProductModelAssetRepository shopProductModelAssetRepository;
@Mock
private FilamentVariantRepository filamentVariantRepository;
@Mock
private PublicMediaQueryService publicMediaQueryService;
@Mock
private ShopStorageService shopStorageService;
private PublicShopCatalogService service;
@BeforeEach
void setUp() {
service = new PublicShopCatalogService(
shopCategoryRepository,
shopProductRepository,
shopProductVariantRepository,
shopProductModelAssetRepository,
filamentVariantRepository,
publicMediaQueryService,
shopStorageService
);
}
@Test
void getProductCatalog_shouldExposePublicPathAsSegment() {
ShopCategory category = buildCategory();
ShopProduct product = buildProduct(category);
ShopProductVariant variant = buildVariant(product);
stubPublicCatalog(category, product, variant);
ShopProductCatalogResponseDto response = service.getProductCatalog(null, false, "en");
assertEquals(1, response.products().size());
assertEquals("12345678-bike-wall-hanger", response.products().getFirst().publicPath());
assertEquals("/en/shop/p/12345678-bike-wall-hanger", response.products().getFirst().localizedPaths().get("en"));
assertEquals("/it/shop/p/12345678-supporto-bici", response.products().getFirst().localizedPaths().get("it"));
}
@Test
void getProduct_shouldExposePublicPathAsSegment() {
ShopCategory category = buildCategory();
ShopProduct product = buildProduct(category);
ShopProductVariant variant = buildVariant(product);
stubPublicCatalog(category, product, variant);
ShopProductDetailDto response = service.getProduct("bike-wall-hanger", "en");
assertEquals("12345678-bike-wall-hanger", response.publicPath());
assertEquals("/en/shop/p/12345678-bike-wall-hanger", response.localizedPaths().get("en"));
assertEquals("/it/shop/p/12345678-supporto-bici", response.localizedPaths().get("it"));
}
@Test
void getProductByPublicPath_shouldResolveLocalizedSegment() {
ShopCategory category = buildCategory();
ShopProduct product = buildProduct(category);
ShopProductVariant variant = buildVariant(product);
stubPublicCatalog(category, product, variant);
ShopProductDetailDto response = service.getProductByPublicPath("12345678-bike-wall-hanger", "en");
assertEquals("bike-wall-hanger", response.slug());
assertEquals("12345678-bike-wall-hanger", response.publicPath());
assertEquals("/en/shop/p/12345678-bike-wall-hanger", response.localizedPaths().get("en"));
}
@Test
void getProductByIdPrefix_shouldResolveLocalizedProduct() {
ShopCategory category = buildCategory();
ShopProduct product = buildProduct(category);
ShopProductVariant variant = buildVariant(product);
stubPublicCatalog(category, product, variant);
ShopProductDetailDto response = service.getProductByIdPrefix("12345678", "de");
assertEquals("bike-wall-hanger", response.slug());
assertEquals("12345678-bike-wall-hanger", response.publicPath());
assertEquals("/de/shop/p/12345678-bike-wall-hanger", response.localizedPaths().get("de"));
}
@Test
void getProductByPublicPath_shouldRejectNonCanonicalSegment() {
ShopCategory category = buildCategory();
ShopProduct product = buildProduct(category);
ShopProductVariant variant = buildVariant(product);
stubPublicCatalog(category, product, variant);
ResponseStatusException exception = assertThrows(
ResponseStatusException.class,
() -> service.getProductByPublicPath("12345678-wrong-tail", "en")
);
assertEquals(404, exception.getStatusCode().value());
}
private void stubPublicCatalog(ShopCategory category, ShopProduct product, ShopProductVariant variant) {
when(shopCategoryRepository.findAllByIsActiveTrueOrderBySortOrderAscNameAsc()).thenReturn(List.of(category));
when(shopProductRepository.findAllByIsActiveTrueOrderByIsFeaturedDescSortOrderAscNameAsc()).thenReturn(List.of(product));
when(shopProductVariantRepository.findByProduct_IdInAndIsActiveTrueOrderBySortOrderAscColorNameAsc(anyList()))
.thenReturn(List.of(variant));
when(shopProductModelAssetRepository.findByProduct_IdIn(anyList())).thenReturn(List.of());
when(filamentVariantRepository.findByIsActiveTrue()).thenReturn(List.of());
when(publicMediaQueryService.getUsageMediaMap(anyString(), anyList(), anyString())).thenReturn(Map.of());
}
private ShopCategory buildCategory() {
ShopCategory category = new ShopCategory();
category.setId(UUID.fromString("21111111-1111-1111-1111-111111111111"));
category.setSlug("accessori");
category.setName("Accessori");
category.setNameIt("Accessori");
category.setNameEn("Accessories");
category.setIsActive(true);
category.setSortOrder(0);
return category;
}
private ShopProduct buildProduct(ShopCategory category) {
ShopProduct product = new ShopProduct();
product.setId(UUID.fromString("12345678-abcd-4abc-9abc-1234567890ab"));
product.setCategory(category);
product.setSlug("bike-wall-hanger");
product.setName("Bike Wall-Hanger");
product.setNameIt("Supporto bici");
product.setNameEn("Bike Wall-Hanger");
product.setIsActive(true);
product.setIsFeatured(true);
product.setSortOrder(0);
return product;
}
private ShopProductVariant buildVariant(ShopProduct product) {
ShopProductVariant variant = new ShopProductVariant();
variant.setId(UUID.fromString("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"));
variant.setProduct(product);
variant.setVariantLabel("PLA");
variant.setColorName("Grigio");
variant.setInternalMaterialCode("PLA");
variant.setPriceChf(new BigDecimal("29.90"));
variant.setIsActive(true);
variant.setIsDefault(true);
variant.setSortOrder(0);
return variant;
}
}

View File

@@ -92,15 +92,15 @@ class ShopSitemapServiceTest {
assertTrue(xml.contains("<loc>https://3d-fab.ch/en/shop/accessori</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/de/shop/accessori</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/fr/shop/accessori</loc>"));
assertTrue(xml.contains("hreflang=\"en\" href=\"https://3d-fab.ch/en/shop/accessori\""));
assertTrue(xml.contains("hreflang=\"en-CH\" href=\"https://3d-fab.ch/en/shop/accessori\""));
assertFalse(xml.contains("https://3d-fab.ch/it/shop/bozza"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/it/shop/p/123e4567-supporto-bici</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/en/shop/p/123e4567-bike-holder</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/de/shop/p/123e4567-fahrrad-halter</loc>"));
assertTrue(xml.contains("<loc>https://3d-fab.ch/fr/shop/p/123e4567-support-velo</loc>"));
assertTrue(xml.contains("hreflang=\"en\" href=\"https://3d-fab.ch/en/shop/p/123e4567-bike-holder\""));
assertTrue(xml.contains("hreflang=\"de\" href=\"https://3d-fab.ch/de/shop/p/123e4567-fahrrad-halter\""));
assertTrue(xml.contains("hreflang=\"en-CH\" href=\"https://3d-fab.ch/en/shop/p/123e4567-bike-holder\""));
assertTrue(xml.contains("hreflang=\"de-CH\" href=\"https://3d-fab.ch/de/shop/p/123e4567-fahrrad-halter\""));
assertTrue(xml.contains("hreflang=\"x-default\" href=\"https://3d-fab.ch/it/shop/p/123e4567-supporto-bici\""));
assertTrue(xml.contains("<lastmod>2026-03-11T07:30:00Z</lastmod>"));
assertFalse(xml.contains("33333333-draft"));

View File

@@ -62,6 +62,8 @@ services:
container_name: print-calculator-frontend-${ENV}
ports:
- "${FRONTEND_PORT}:80"
environment:
- SSR_INTERNAL_API_ORIGIN=http://backend:8000
depends_on:
- backend
restart: always

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,23 @@
{
"name": "3D fab",
"short_name": "3D fab",
"description": "Stampa 3D su misura con preventivo online immediato.",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"icons": [
{
"src": "/assets/images/Fav-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/images/Fav-icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
]
}

View File

@@ -2,81 +2,81 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://3d-fab.ch/it</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/" />
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://3d-fab.ch/en</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/" />
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://3d-fab.ch/de</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/" />
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/" />
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://3d-fab.ch/it/calculator/basic</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/basic" />
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/calculator/basic</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/basic" />
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/calculator/basic</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/basic" />
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/calculator/basic</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/calculator/basic" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/calculator/basic" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/calculator/basic" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/calculator/basic" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/basic" />
<changefreq>weekly</changefreq>
<priority>0.9</priority>
@@ -84,40 +84,40 @@
<url>
<loc>https://3d-fab.ch/it/calculator/advanced</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/advanced" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/calculator/advanced</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/advanced" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/calculator/advanced</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/advanced" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/calculator/advanced</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/calculator/advanced" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/calculator/advanced" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
@@ -125,40 +125,40 @@
<url>
<loc>https://3d-fab.ch/it/shop</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/shop" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/shop</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/shop" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/shop</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/shop" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/shop</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/shop" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/shop" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/shop" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/shop" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/shop" />
<changefreq>weekly</changefreq>
<priority>0.8</priority>
@@ -166,40 +166,40 @@
<url>
<loc>https://3d-fab.ch/it/about</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/about" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/about</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/about" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/about</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/about" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/about</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/about" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/about" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/about" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/about" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/about" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
@@ -207,40 +207,40 @@
<url>
<loc>https://3d-fab.ch/it/contact</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/contact" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/contact</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/contact" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/contact</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/contact" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/contact</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/contact" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/contact" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/contact" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/contact" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/contact" />
<changefreq>monthly</changefreq>
<priority>0.7</priority>
@@ -248,40 +248,40 @@
<url>
<loc>https://3d-fab.ch/it/privacy</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/privacy" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/privacy</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/privacy" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/privacy</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/privacy" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/privacy</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/privacy" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/privacy" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/privacy" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/privacy" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/privacy" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
@@ -289,40 +289,40 @@
<url>
<loc>https://3d-fab.ch/it/terms</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/terms" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/en/terms</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/terms" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/de/terms</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/terms" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://3d-fab.ch/fr/terms</loc>
<xhtml:link rel="alternate" hreflang="it" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="it-CH" href="https://3d-fab.ch/it/terms" />
<xhtml:link rel="alternate" hreflang="en-CH" href="https://3d-fab.ch/en/terms" />
<xhtml:link rel="alternate" hreflang="de-CH" href="https://3d-fab.ch/de/terms" />
<xhtml:link rel="alternate" hreflang="fr-CH" href="https://3d-fab.ch/fr/terms" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://3d-fab.ch/it/terms" />
<changefreq>yearly</changefreq>
<priority>0.4</priority>

View File

@@ -1 +1,14 @@
<router-outlet></router-outlet>
@if (siteIntroState() !== "hidden") {
<div
class="site-intro"
[class.site-intro--closing]="siteIntroState() === 'closing'"
aria-hidden="true"
>
<app-brand-animation-logo
class="site-intro__logo"
variant="site-intro"
></app-brand-animation-logo>
</div>
}

View File

@@ -0,0 +1,40 @@
.site-intro {
position: fixed;
inset: 0;
z-index: 2000;
display: grid;
place-items: center;
background: var(--color-bg);
pointer-events: none;
opacity: 1;
transition: opacity 0.24s ease-out;
}
.site-intro--closing {
opacity: 0;
}
.site-intro__logo {
width: min(calc(100vw - 2rem), 23rem);
--brand-animation-width: 23rem;
--brand-animation-height: 7.1rem;
--brand-animation-letter-width: 3.75rem;
--brand-animation-scale: 0.88;
--brand-animation-width-mobile: 16.8rem;
--brand-animation-height-mobile: 5.3rem;
--brand-animation-letter-width-mobile: 2.8rem;
--brand-animation-scale-mobile: 0.68;
--brand-animation-site-intro-duration: 1.05s;
justify-self: center;
align-self: center;
opacity: 1;
transform: scale(1);
transition:
opacity 0.24s ease-out,
transform 0.24s ease-out;
}
.site-intro--closing .site-intro__logo {
opacity: 0;
transform: scale(0.985);
}

View File

@@ -1,14 +1,50 @@
import { Component, inject } from '@angular/core';
import {
afterNextRender,
Component,
DestroyRef,
Inject,
Optional,
PLATFORM_ID,
inject,
signal,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { SeoService } from './core/services/seo.service';
import { BrandAnimationLogoComponent } from './shared/components/brand-animation-logo/brand-animation-logo.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
imports: [RouterOutlet, BrandAnimationLogoComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
private readonly seoService = inject(SeoService);
private readonly destroyRef = inject(DestroyRef);
readonly siteIntroState = signal<'hidden' | 'active' | 'closing'>('hidden');
constructor(@Optional() @Inject(PLATFORM_ID) platformId?: Object) {
if (!isPlatformBrowser(platformId ?? 'browser')) {
return;
}
afterNextRender(() => {
this.siteIntroState.set('active');
const closeTimeoutId = window.setTimeout(() => {
this.siteIntroState.set('closing');
}, 1020);
const hideTimeoutId = window.setTimeout(() => {
this.siteIntroState.set('hidden');
}, 1280);
this.destroyRef.onDestroy(() => {
window.clearTimeout(closeTimeoutId);
window.clearTimeout(hideTimeoutId);
});
});
}
}

View File

@@ -28,21 +28,12 @@ import {
import { serverOriginInterceptor } from './core/interceptors/server-origin.interceptor';
import { catchError, firstValueFrom, of } from 'rxjs';
import { StaticTranslateLoader } from './core/i18n/static-translate.loader';
type SupportedLang = 'it' | 'en' | 'de' | 'fr';
const SUPPORTED_LANGS: readonly SupportedLang[] = ['it', 'en', 'de', 'fr'];
function resolveLangFromUrl(url: string): SupportedLang {
const firstSegment = (url || '/')
.split('?')[0]
.split('#')[0]
.split('/')
.filter(Boolean)[0]
?.toLowerCase();
return SUPPORTED_LANGS.includes(firstSegment as SupportedLang)
? (firstSegment as SupportedLang)
: 'it';
}
import {
getNavigatorLanguagePreferences,
parseAcceptLanguage,
resolveInitialLanguage,
SUPPORTED_LANGS,
} from './core/i18n/language-resolution';
export const appConfig: ApplicationConfig = {
providers: [
@@ -52,7 +43,7 @@ export const appConfig: ApplicationConfig = {
withComponentInputBinding(),
withViewTransitions(),
withInMemoryScrolling({
scrollPositionRestoration: 'top',
scrollPositionRestoration: 'enabled',
}),
),
provideHttpClient(
@@ -72,13 +63,21 @@ export const appConfig: ApplicationConfig = {
const router = inject(Router);
const request = inject(REQUEST, { optional: true }) as {
url?: string;
headers?: Record<string, string | string[] | undefined>;
} | null;
translate.addLangs([...SUPPORTED_LANGS]);
translate.setFallbackLang('it');
const requestedUrl =
(typeof request?.url === 'string' && request.url) || router.url || '/';
const lang = resolveLangFromUrl(requestedUrl);
const lang = resolveInitialLanguage({
url: requestedUrl,
preferredLanguages: request
? parseAcceptLanguage(readRequestHeader(request, 'accept-language'))
: getNavigatorLanguagePreferences(
typeof navigator === 'undefined' ? null : navigator,
),
});
return firstValueFrom(
translate.use(lang).pipe(
@@ -96,3 +95,21 @@ export const appConfig: ApplicationConfig = {
provideClientHydration(withEventReplay()),
],
};
function readRequestHeader(
request: {
headers?: Record<string, string | string[] | undefined>;
} | null,
headerName: string,
): string | null {
if (!request?.headers) {
return null;
}
const headerValue = request.headers[headerName.toLowerCase()];
if (Array.isArray(headerValue)) {
return headerValue[0] ?? null;
}
return typeof headerValue === 'string' ? headerValue : null;
}

View File

@@ -134,6 +134,31 @@ const appChildRoutes: Routes = [
];
export const routes: Routes = [
{
path: ':lang/calculator/animation-test',
canMatch: [langPrefixCanMatch],
loadComponent: () =>
import('./features/calculator/calculator-animation-test.component').then(
(m) => m.CalculatorAnimationTestComponent,
),
data: {
seoTitleKey: 'SEO.ROUTES.CALCULATOR.TITLE',
seoDescriptionKey: 'SEO.ROUTES.CALCULATOR.DESCRIPTION',
seoRobots: 'noindex, nofollow',
},
},
{
path: 'calculator/animation-test',
loadComponent: () =>
import('./features/calculator/calculator-animation-test.component').then(
(m) => m.CalculatorAnimationTestComponent,
),
data: {
seoTitleKey: 'SEO.ROUTES.CALCULATOR.TITLE',
seoDescriptionKey: 'SEO.ROUTES.CALCULATOR.DESCRIPTION',
seoRobots: 'noindex, nofollow',
},
},
{
path: ':lang',
canMatch: [langPrefixCanMatch],

View File

@@ -0,0 +1,135 @@
export type SupportedLang = 'it' | 'en' | 'de' | 'fr';
export const SUPPORTED_LANGS: readonly SupportedLang[] = [
'it',
'en',
'de',
'fr',
];
type InitialLanguageOptions = {
url?: string | null;
preferredLanguages?: readonly string[] | null;
fallbackLang?: SupportedLang;
};
type NavigatorLike = {
language?: string;
languages?: readonly string[];
};
export function resolveInitialLanguage({
url,
preferredLanguages,
fallbackLang = 'it',
}: InitialLanguageOptions): SupportedLang {
const explicitLang = resolveExplicitLanguageFromUrl(url);
if (explicitLang) {
return explicitLang;
}
for (const candidate of preferredLanguages ?? []) {
const normalized = normalizeSupportedLanguage(candidate);
if (normalized) {
return normalized;
}
}
return fallbackLang;
}
export function parseAcceptLanguage(
header: string | null | undefined,
): string[] {
if (!header) {
return [];
}
return header
.split(',')
.map((entry, index) => {
const [rawTag, ...params] = entry.split(';').map((part) => part.trim());
if (!rawTag) {
return null;
}
const qualityParam = params.find((param) => param.startsWith('q='));
const quality = qualityParam
? Number.parseFloat(qualityParam.slice(2))
: 1;
return {
tag: rawTag,
quality: Number.isFinite(quality) ? quality : 0,
index,
};
})
.filter(
(
entry,
): entry is {
tag: string;
quality: number;
index: number;
} => entry !== null && entry.quality > 0,
)
.sort(
(left, right) => right.quality - left.quality || left.index - right.index,
)
.map((entry) => entry.tag);
}
export function getNavigatorLanguagePreferences(
navigatorLike: NavigatorLike | null | undefined,
): string[] {
if (!navigatorLike) {
return [];
}
const orderedLanguages = [
...(Array.isArray(navigatorLike.languages) ? navigatorLike.languages : []),
];
if (
typeof navigatorLike.language === 'string' &&
navigatorLike.language &&
!orderedLanguages.includes(navigatorLike.language)
) {
orderedLanguages.push(navigatorLike.language);
}
return orderedLanguages;
}
function resolveExplicitLanguageFromUrl(
url: string | null | undefined,
): SupportedLang | null {
const normalizedUrl = String(url ?? '/');
const [pathAndQuery] = normalizedUrl.split('#', 1);
const [rawPath, rawQuery] = pathAndQuery.split('?', 2);
const firstSegment = rawPath.split('/').filter(Boolean)[0];
const pathLanguage = normalizeSupportedLanguage(firstSegment);
if (pathLanguage) {
return pathLanguage;
}
const queryLanguage = new URLSearchParams(rawQuery ?? '').get('lang');
return normalizeSupportedLanguage(queryLanguage);
}
function normalizeSupportedLanguage(
rawLanguage: string | null | undefined,
): SupportedLang | null {
if (typeof rawLanguage !== 'string') {
return null;
}
const normalized = rawLanguage.trim().toLowerCase();
if (!normalized || normalized === '*') {
return null;
}
const [baseLanguage] = normalized.split('-', 1);
return SUPPORTED_LANGS.includes(baseLanguage as SupportedLang)
? (baseLanguage as SupportedLang)
: null;
}

View File

@@ -14,7 +14,10 @@ type SupportedLang = 'it' | 'en' | 'de' | 'fr';
const FALLBACK_LANG: SupportedLang = 'it';
const translationCache = new Map<SupportedLang, Promise<TranslationObject>>();
const translationLoaders: Record<SupportedLang, () => Promise<TranslationObject>> = {
const translationLoaders: Record<
SupportedLang,
() => Promise<TranslationObject>
> = {
it: () =>
import('../../../assets/i18n/it.json').then(
(module) => module.default as TranslationObject,
@@ -51,8 +54,9 @@ export class StaticTranslateLoader implements TranslateLoader {
}
private loadTranslation(lang: SupportedLang): Promise<TranslationObject> {
const transferStateKey =
makeStateKey<TranslationObject>(`i18n:${lang.toLowerCase()}`);
const transferStateKey = makeStateKey<TranslationObject>(
`i18n:${lang.toLowerCase()}`,
);
if (
isPlatformBrowser(this.platformId) &&
this.transferState.hasKey(transferStateKey)

View File

@@ -0,0 +1,196 @@
import { TestBed } from '@angular/core/testing';
import { HttpClient } from '@angular/common/http';
import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing';
import { REQUEST } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { serverOriginInterceptor } from './server-origin.interceptor';
type TestGlobal = typeof globalThis & {
__SSR_INTERNAL_API_ORIGIN__?: string;
};
describe('serverOriginInterceptor', () => {
let http: HttpClient;
let httpMock: HttpTestingController;
const testGlobal = globalThis as TestGlobal;
const originalInternalApiOrigin = testGlobal.__SSR_INTERNAL_API_ORIGIN__;
beforeEach(() => {
delete testGlobal.__SSR_INTERNAL_API_ORIGIN__;
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([serverOriginInterceptor])),
provideHttpClientTesting(),
{
provide: REQUEST,
useValue: {
protocol: 'https',
url: '/de/shop/p/91823f84-bike-wall-hanger',
headers: {
host: 'dev.3d-fab.ch',
authorization: 'Basic dGVzdDp0ZXN0',
cookie: 'session=abc123',
'accept-language': 'de-CH,de;q=0.9,en;q=0.8',
},
},
},
],
});
http = TestBed.inject(HttpClient);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
if (originalInternalApiOrigin) {
testGlobal.__SSR_INTERNAL_API_ORIGIN__ = originalInternalApiOrigin;
return;
}
delete testGlobal.__SSR_INTERNAL_API_ORIGIN__;
});
it('rewrites relative SSR URLs to the incoming origin and forwards auth headers', () => {
http.get('/api/shop/products/by-path/example?lang=de').subscribe();
const request = httpMock.expectOne(
'https://dev.3d-fab.ch/api/shop/products/by-path/example?lang=de',
);
expect(request.request.headers.get('authorization')).toBe(
'Basic dGVzdDp0ZXN0',
);
expect(request.request.headers.get('cookie')).toBe('session=abc123');
expect(request.request.headers.get('accept-language')).toBe(
'de-CH,de;q=0.9,en;q=0.8',
);
request.flush({});
});
it('does not overwrite explicit request headers', () => {
http
.get('/api/shop/products', {
headers: {
authorization: 'Bearer explicit-token',
},
})
.subscribe();
const request = httpMock.expectOne(
'https://dev.3d-fab.ch/api/shop/products',
);
expect(request.request.headers.get('authorization')).toBe(
'Bearer explicit-token',
);
expect(request.request.headers.get('cookie')).toBe('session=abc123');
request.flush({});
});
it('uses the internal SSR API origin for public shop discovery calls', () => {
testGlobal.__SSR_INTERNAL_API_ORIGIN__ = 'http://backend:8000/';
http.get('/api/shop/products/by-id-prefix/91823f84?lang=de').subscribe();
const request = httpMock.expectOne(
'http://backend:8000/api/shop/products/by-id-prefix/91823f84?lang=de',
);
expect(request.request.headers.get('authorization')).toBe(
'Basic dGVzdDp0ZXN0',
);
expect(request.request.headers.get('cookie')).toBe('session=abc123');
expect(request.request.headers.get('accept-language')).toBe(
'de-CH,de;q=0.9,en;q=0.8',
);
request.flush({});
});
it('bypasses the public origin even when the proxy strips authorization on shop SSR requests', () => {
testGlobal.__SSR_INTERNAL_API_ORIGIN__ = 'http://backend:8000/';
TestBed.resetTestingModule();
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([serverOriginInterceptor])),
provideHttpClientTesting(),
{
provide: REQUEST,
useValue: {
protocol: 'https',
url: '/de/shop/p/91823f84-bike-wall-hanger',
headers: {
host: 'dev.3d-fab.ch',
cookie: 'session=abc123',
'accept-language': 'de-CH,de;q=0.9,en;q=0.8',
},
},
},
],
});
http = TestBed.inject(HttpClient);
httpMock = TestBed.inject(HttpTestingController);
http.get('/api/shop/products/by-id-prefix/91823f84?lang=de').subscribe();
const request = httpMock.expectOne(
'http://backend:8000/api/shop/products/by-id-prefix/91823f84?lang=de',
);
expect(request.request.headers.get('authorization')).toBeNull();
expect(request.request.headers.get('cookie')).toBe('session=abc123');
expect(request.request.headers.get('accept-language')).toBe(
'de-CH,de;q=0.9,en;q=0.8',
);
request.flush({});
});
it('keeps transactional shop API calls on the public origin', () => {
testGlobal.__SSR_INTERNAL_API_ORIGIN__ = 'http://backend:8000/';
http.get('/api/shop/cart').subscribe();
const request = httpMock.expectOne('https://dev.3d-fab.ch/api/shop/cart');
expect(request.request.headers.get('authorization')).toBe(
'Basic dGVzdDp0ZXN0',
);
request.flush({});
});
it('keeps non-shop pages on the public origin even for public shop APIs', () => {
testGlobal.__SSR_INTERNAL_API_ORIGIN__ = 'http://backend:8000/';
TestBed.resetTestingModule();
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([serverOriginInterceptor])),
provideHttpClientTesting(),
{
provide: REQUEST,
useValue: {
protocol: 'https',
url: '/de/checkout?session=abc',
headers: {
host: 'dev.3d-fab.ch',
cookie: 'session=abc123',
'accept-language': 'de-CH,de;q=0.9,en;q=0.8',
},
},
},
],
});
http = TestBed.inject(HttpClient);
httpMock = TestBed.inject(HttpTestingController);
http.get('/api/shop/products/by-id-prefix/91823f84?lang=de').subscribe();
const request = httpMock.expectOne(
'https://dev.3d-fab.ch/api/shop/products/by-id-prefix/91823f84?lang=de',
);
expect(request.request.headers.get('authorization')).toBeNull();
expect(request.request.headers.get('cookie')).toBe('session=abc123');
request.flush({});
});
});

View File

@@ -5,6 +5,27 @@ import {
resolveRequestOrigin,
} from '../../../core/request-origin';
type ServerRequestLike = RequestLike & {
originalUrl?: string;
url?: string;
};
const FORWARDED_REQUEST_HEADERS = [
'authorization',
'cookie',
'accept-language',
] as const;
const SHOP_DISCOVERY_API_PATTERNS = [
/^\/api\/shop\/categories(?:\/[^/?#]+)?$/i,
/^\/api\/shop\/products$/i,
/^\/api\/shop\/products\/by-id-prefix\/[^/?#]+$/i,
/^\/api\/shop\/products\/by-path\/[^/?#]+$/i,
/^\/api\/shop\/products\/[^/?#]+$/i,
] as const;
const SHOP_PAGE_PATH_PATTERN = /^\/(?:it|en|de|fr)\/shop(?:\/.*)?$/i;
function isAbsoluteUrl(url: string): boolean {
return /^[a-z][a-z\d+\-.]*:/i.test(url) || url.startsWith('//');
}
@@ -14,17 +35,135 @@ function normalizeRelativePath(url: string): string {
return withoutDot.startsWith('/') ? withoutDot : `/${withoutDot}`;
}
function stripQueryAndHash(url: string): string {
return String(url ?? '').split(/[?#]/, 1)[0] || '/';
}
function normalizeOrigin(origin: string): string {
return origin.replace(/\/+$/, '');
}
function readRequestHeader(
request: RequestLike | null,
name: (typeof FORWARDED_REQUEST_HEADERS)[number],
): string | null {
const normalizedName = name.toLowerCase();
const headerValue =
request?.headers?.[normalizedName] ?? request?.get?.(normalizedName);
if (Array.isArray(headerValue)) {
return headerValue[0] ?? null;
}
return typeof headerValue === 'string' ? headerValue : null;
}
function readRequestPath(request: ServerRequestLike | null): string | null {
const rawPath =
(typeof request?.originalUrl === 'string' && request.originalUrl) ||
(typeof request?.url === 'string' && request.url) ||
null;
if (!rawPath) {
return null;
}
if (isAbsoluteUrl(rawPath)) {
try {
return stripQueryAndHash(new URL(rawPath).pathname || '/');
} catch {
return null;
}
}
return stripQueryAndHash(rawPath.startsWith('/') ? rawPath : `/${rawPath}`);
}
function isPublicShopPageRequest(request: ServerRequestLike | null): boolean {
const requestPath = readRequestPath(request);
return !!requestPath && SHOP_PAGE_PATH_PATTERN.test(requestPath);
}
function isPublicShopDiscoveryApi(url: string): boolean {
const normalizedPath = stripQueryAndHash(normalizeRelativePath(url));
return SHOP_DISCOVERY_API_PATTERNS.some((pattern) =>
pattern.test(normalizedPath),
);
}
function readInternalApiOrigin(): string | null {
const globalObject = globalThis as {
__SSR_INTERNAL_API_ORIGIN__?: string;
process?: {
env?: Record<string, string | undefined>;
};
};
const explicitOverride =
typeof globalObject.__SSR_INTERNAL_API_ORIGIN__ === 'string'
? globalObject.__SSR_INTERNAL_API_ORIGIN__
: null;
const env = (
globalObject as {
process?: {
env?: Record<string, string | undefined>;
};
}
).process?.env;
const rawValue = explicitOverride ?? env?.['SSR_INTERNAL_API_ORIGIN'];
if (typeof rawValue !== 'string') {
return null;
}
const normalized = rawValue.trim();
return normalized ? normalizeOrigin(normalized) : null;
}
function resolveApiOrigin(
request: ServerRequestLike | null,
relativeUrl: string,
): string | null {
const internalOrigin = readInternalApiOrigin();
if (
internalOrigin &&
isPublicShopPageRequest(request) &&
isPublicShopDiscoveryApi(relativeUrl)
) {
return internalOrigin;
}
return resolveRequestOrigin(request);
}
export const serverOriginInterceptor: HttpInterceptorFn = (req, next) => {
if (isAbsoluteUrl(req.url)) {
return next(req);
}
const request = inject(REQUEST, { optional: true }) as RequestLike | null;
const origin = resolveRequestOrigin(request);
const request = inject(REQUEST, {
optional: true,
}) as ServerRequestLike | null;
const origin = resolveApiOrigin(request, req.url);
if (!origin) {
return next(req);
}
const absoluteUrl = `${origin}${normalizeRelativePath(req.url)}`;
return next(req.clone({ url: absoluteUrl }));
const absoluteUrl = `${normalizeOrigin(origin)}${normalizeRelativePath(req.url)}`;
const forwardedHeaders = FORWARDED_REQUEST_HEADERS.reduce<
Record<string, string>
>((headers, name) => {
if (req.headers.has(name)) {
return headers;
}
const value = readRequestHeader(request, name);
if (value) {
headers[name] = value;
}
return headers;
}, {});
return next(
req.clone({
url: absoluteUrl,
setHeaders: forwardedHeaders,
}),
);
};

View File

@@ -1,7 +1,11 @@
<footer class="footer">
<div class="container footer-inner">
<div class="col">
<span class="brand">3D fab</span>
<img
class="brand"
src="/assets/images/brand-logo-white.svg"
alt="3D Fab"
/>
<p class="copyright">&copy; 2026 3D fab.</p>
</div>
@@ -18,10 +22,30 @@
</div>
<div class="col social">
<!-- Social Placeholders -->
<div class="social-icon"></div>
<div class="social-icon"></div>
<div class="social-icon"></div>
<div class="social-link-row">
<span class="social-name">Joe Küng:</span>
<a
class="social-icon-link"
href="https://www.linkedin.com/in/joe-k%C3%BCng-31831828b/"
target="_blank"
rel="noopener noreferrer"
aria-label="Joe Küng LinkedIn"
>
<span class="social-icon-linkedin" aria-hidden="true"></span>
</a>
</div>
<div class="social-link-row">
<span class="social-name">Matteo Caletti:</span>
<a
class="social-icon-link"
href="https://www.linkedin.com/in/matteo-caletti-94291a3b6/"
target="_blank"
rel="noopener noreferrer"
aria-label="Matteo Caletti LinkedIn"
>
<span class="social-icon-linkedin" aria-hidden="true"></span>
</a>
</div>
</div>
</div>
</footer>

View File

@@ -38,9 +38,10 @@
}
.brand {
font-weight: 700;
color: white;
display: block;
width: auto;
height: 1.85rem;
max-width: min(9.25rem, 46vw);
margin-bottom: var(--space-2);
}
.copyright {
@@ -65,11 +66,69 @@
.social {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-3);
}
.social-icon {
width: 24px;
height: 24px;
background-color: var(--color-neutral-800);
border-radius: 50%;
.social-link-row {
display: flex;
align-items: center;
gap: var(--space-3);
}
.social-name {
color: var(--color-neutral-200);
font-size: 0.875rem;
}
.social-icon-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: var(--color-neutral-50);
color: #0a66c2;
transition:
transform 0.2s ease,
background-color 0.2s ease,
color 0.2s ease;
&:hover {
background-color: #0a66c2;
color: var(--color-neutral-50);
transform: translateY(-1px);
}
&:focus-visible {
outline: 2px solid var(--color-secondary-500);
outline-offset: 2px;
}
}
.social-icon-linkedin {
display: block;
width: 1rem;
height: 1rem;
background-color: currentColor;
mask-image: url("/assets/images/SVG/linkedin-svgrepo-com.svg");
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
-webkit-mask-image: url("/assets/images/SVG/linkedin-svgrepo-com.svg");
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: contain;
}
@media (max-width: 768px) {
.social {
align-items: center;
}
.social-link-row {
justify-content: center;
}
}

View File

@@ -1,8 +1,15 @@
<header class="navbar">
<div class="container navbar-inner">
<a [routerLink]="langService.localizedPath('/')" class="brand"
>3D <span class="highlight">fab</span></a
>
<a [routerLink]="langService.localizedPath('/')" class="brand">
<img
class="brand-logo"
ngSrc="/assets/images/Asset%202.svg"
alt="3D Fab"
width="380"
height="86"
priority
/>
</a>
<div
class="mobile-toggle"

View File

@@ -14,13 +14,16 @@
justify-content: space-between;
}
.brand {
font-size: 1.25rem;
font-weight: 700;
color: var(--color-text);
display: inline-flex;
align-items: center;
text-decoration: none;
}
.highlight {
color: var(--color-brand);
.brand-logo {
display: block;
width: auto;
height: 2.1rem;
max-width: min(11rem, 40vw);
}
.nav-links {

View File

@@ -1,4 +1,4 @@
import { CommonModule } from '@angular/common';
import { CommonModule, NgOptimizedImage } from '@angular/common';
import {
afterNextRender,
Component,
@@ -30,7 +30,13 @@ import {
@Component({
selector: 'app-navbar',
standalone: true,
imports: [CommonModule, RouterLink, RouterLinkActive, TranslateModule],
imports: [
CommonModule,
RouterLink,
RouterLinkActive,
TranslateModule,
NgOptimizedImage,
],
templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.scss'],
})

View File

@@ -1,7 +1,13 @@
import { Subject } from 'rxjs';
import { DefaultUrlSerializer, Router, UrlTree } from '@angular/router';
import {
DefaultUrlSerializer,
NavigationEnd,
Router,
UrlTree,
} from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { LanguageService } from './language.service';
import { RequestLike } from '../../../core/request-origin';
describe('LanguageService', () => {
function createTranslateMock() {
@@ -60,7 +66,14 @@ describe('LanguageService', () => {
parseUrl: (url: string) => serializer.parse(url),
createUrlTree,
serializeUrl: (tree: UrlTree) => serializer.serialize(tree),
navigateByUrl: jasmine.createSpy('navigateByUrl'),
navigateByUrl: jasmine
.createSpy('navigateByUrl')
.and.callFake((tree: UrlTree) => {
const nextUrl = serializer.serialize(tree);
router.url = nextUrl;
events$.next(new NavigationEnd(1, nextUrl, nextUrl));
return Promise.resolve(true);
}),
};
return router as unknown as Router;
@@ -70,9 +83,14 @@ describe('LanguageService', () => {
const translate = createTranslateMock();
const router = createRouterMock('/calculator?session=abc');
const navigateSpy = router.navigateByUrl as unknown as jasmine.Spy;
const request: RequestLike = {
headers: {
'accept-language': 'it-CH,it;q=0.9,en;q=0.8',
},
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const service = new LanguageService(translate, router);
const service = new LanguageService(translate, router, request);
expect(translate.use).toHaveBeenCalledWith('it');
expect((translate as any).setFallbackLang).toHaveBeenCalledWith('it');
@@ -85,6 +103,48 @@ describe('LanguageService', () => {
expect(navOptions.replaceUrl).toBeTrue();
});
it('uses the preferred browser language on the root URL', () => {
const translate = createTranslateMock();
const router = createRouterMock('/');
const navigateSpy = router.navigateByUrl as unknown as jasmine.Spy;
const request: RequestLike = {
headers: {
'accept-language': 'de-CH,de;q=0.9,en;q=0.8,it;q=0.7',
},
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const service = new LanguageService(translate, router, request);
expect(translate.use).toHaveBeenCalledWith('de');
expect(navigateSpy).toHaveBeenCalledTimes(1);
const firstCall = navigateSpy.calls.mostRecent();
const tree = firstCall.args[0] as UrlTree;
expect(router.serializeUrl(tree)).toBe('/de');
});
it('uses the default language for non-root URLs without a language prefix', () => {
const translate = createTranslateMock();
const router = createRouterMock('/calculator?session=abc');
const navigateSpy = router.navigateByUrl as unknown as jasmine.Spy;
const request: RequestLike = {
headers: {
'accept-language': 'de-CH,de;q=0.9,en;q=0.8,it;q=0.7',
},
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const service = new LanguageService(translate, router, request);
expect(translate.use).toHaveBeenCalledWith('de');
expect(navigateSpy).toHaveBeenCalledTimes(1);
const firstCall = navigateSpy.calls.mostRecent();
const tree = firstCall.args[0] as UrlTree;
expect(router.serializeUrl(tree)).toBe('/it/calculator?session=abc');
});
it('switches language while preserving path and query params', () => {
const translate = createTranslateMock();
const router = createRouterMock('/it/calculator?session=abc&mode=advanced');
@@ -115,4 +175,23 @@ describe('LanguageService', () => {
'/de/contact?topic=seo#form',
);
});
it('switches product pages using the resolved localized route overrides', () => {
const translate = createTranslateMock();
const router = createRouterMock('/it/shop/p/12345678-supporto-cavo');
const navigateSpy = router.navigateByUrl as unknown as jasmine.Spy;
const service = new LanguageService(translate, router);
service.setLocalizedRouteOverrides({
it: '/it/shop/p/12345678-supporto-cavo',
de: '/de/shop/p/12345678-kabelhalter',
});
navigateSpy.calls.reset();
service.switchLang('de');
const call = navigateSpy.calls.mostRecent();
const tree = call.args[0] as UrlTree;
expect(router.serializeUrl(tree)).toBe('/de/shop/p/12345678-kabelhalter');
});
});

View File

@@ -1,4 +1,4 @@
import { Injectable, signal } from '@angular/core';
import { Inject, Injectable, Optional, REQUEST, signal } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {
NavigationEnd,
@@ -6,22 +6,29 @@ import {
Router,
UrlTree,
} from '@angular/router';
import {
getNavigatorLanguagePreferences,
parseAcceptLanguage,
resolveInitialLanguage,
} from '../i18n/language-resolution';
import { RequestLike } from '../../../core/request-origin';
type SupportedLang = 'it' | 'en' | 'de' | 'fr';
type LocalizedRouteOverrides = Partial<Record<SupportedLang, string>>;
@Injectable({
providedIn: 'root',
})
export class LanguageService {
currentLang = signal<'it' | 'en' | 'de' | 'fr'>('it');
private readonly supportedLangs: Array<'it' | 'en' | 'de' | 'fr'> = [
'it',
'en',
'de',
'fr',
];
currentLang = signal<SupportedLang>('it');
private readonly defaultLang: SupportedLang = 'it';
private readonly supportedLangs: SupportedLang[] = ['it', 'en', 'de', 'fr'];
private localizedRouteOverrides: LocalizedRouteOverrides | null = null;
constructor(
private translate: TranslateService,
private router: Router,
@Optional() @Inject(REQUEST) private request: RequestLike | null = null,
) {
this.translate.addLangs(this.supportedLangs);
this.translate.setFallbackLang('it');
@@ -34,13 +41,14 @@ export class LanguageService {
});
const initialTree = this.router.parseUrl(this.router.url);
const initialSegments = this.getPrimarySegments(initialTree);
const queryLang = this.getQueryLang(initialTree);
const initialLang = this.isSupportedLang(initialSegments[0])
? initialSegments[0]
: this.isSupportedLang(queryLang)
? queryLang
: 'it';
const initialLang = resolveInitialLanguage({
url: this.router.url,
preferredLanguages: this.request
? parseAcceptLanguage(this.readRequestHeader('accept-language'))
: getNavigatorLanguagePreferences(
typeof navigator === 'undefined' ? null : navigator,
),
});
this.applyLanguage(initialLang);
this.ensureLanguageInPath(initialTree);
@@ -53,13 +61,21 @@ export class LanguageService {
});
}
switchLang(lang: 'it' | 'en' | 'de' | 'fr') {
switchLang(lang: SupportedLang) {
if (!this.isSupportedLang(lang)) {
return;
}
this.applyLanguage(lang);
const currentTree = this.router.parseUrl(this.router.url);
const localizedRoute = this.resolveLocalizedRouteOverride(
currentTree,
lang,
);
if (localizedRoute) {
this.navigateToLocalizedRoute(currentTree, localizedRoute);
return;
}
const segments = this.getPrimarySegments(currentTree);
let targetSegments: string[];
@@ -77,7 +93,7 @@ export class LanguageService {
this.navigateIfChanged(currentTree, targetSegments);
}
selectedLang(): 'it' | 'en' | 'de' | 'fr' {
selectedLang(): SupportedLang {
const activeLang =
typeof this.translate.currentLang === 'string'
? this.translate.currentLang.toLowerCase()
@@ -110,6 +126,16 @@ export class LanguageService {
return `/${[lang, ...segments].join('/')}${suffix}`;
}
setLocalizedRouteOverrides(
paths: LocalizedRouteOverrides | null | undefined,
): void {
this.localizedRouteOverrides = this.normalizeLocalizedRouteOverrides(paths);
}
clearLocalizedRouteOverrides(): void {
this.localizedRouteOverrides = null;
}
private ensureLanguageInPath(urlTree: UrlTree): void {
const segments = this.getPrimarySegments(urlTree);
@@ -118,23 +144,26 @@ export class LanguageService {
return;
}
const queryLang = this.getQueryLang(urlTree);
const activeLang = this.isSupportedLang(queryLang)
? queryLang
: this.currentLang();
if (activeLang !== this.currentLang()) {
this.applyLanguage(activeLang);
}
let targetSegments: string[];
if (segments.length === 0) {
targetSegments = [activeLang];
} else if (this.looksLikeLangToken(segments[0])) {
targetSegments = [activeLang, ...segments.slice(1)];
} else {
targetSegments = [activeLang, ...segments];
const queryLang = this.getQueryLang(urlTree);
const rootLang = this.isSupportedLang(queryLang)
? queryLang
: this.currentLang();
if (rootLang !== this.currentLang()) {
this.applyLanguage(rootLang);
}
this.navigateIfChanged(urlTree, [rootLang]);
return;
}
if (this.currentLang() !== this.defaultLang) {
this.applyLanguage(this.defaultLang);
}
const targetSegments = this.looksLikeLangToken(segments[0])
? [this.defaultLang, ...segments.slice(1)]
: [this.defaultLang, ...segments];
this.navigateIfChanged(urlTree, targetSegments);
}
@@ -151,12 +180,23 @@ export class LanguageService {
return typeof lang === 'string' ? lang.toLowerCase() : null;
}
private readRequestHeader(headerName: string): string | null {
const headerValue =
this.request?.headers?.[headerName.toLowerCase()] ??
this.request?.get?.(headerName.toLowerCase());
if (Array.isArray(headerValue)) {
return headerValue[0] ?? null;
}
return typeof headerValue === 'string' ? headerValue : null;
}
private isSupportedLang(
lang: string | null | undefined,
): lang is 'it' | 'en' | 'de' | 'fr' {
): lang is SupportedLang {
return (
typeof lang === 'string' &&
this.supportedLangs.includes(lang as 'it' | 'en' | 'de' | 'fr')
this.supportedLangs.includes(lang as SupportedLang)
);
}
@@ -166,7 +206,7 @@ export class LanguageService {
);
}
private applyLanguage(lang: 'it' | 'en' | 'de' | 'fr'): void {
private applyLanguage(lang: SupportedLang): void {
if (this.currentLang() === lang && this.translate.currentLang === lang) {
return;
}
@@ -174,6 +214,88 @@ export class LanguageService {
this.currentLang.set(lang);
}
private resolveLocalizedRouteOverride(
currentTree: UrlTree,
lang: SupportedLang,
): string | null {
const overrides = this.localizedRouteOverrides;
if (!overrides) {
return null;
}
const currentPath = this.getCleanPath(
this.router.serializeUrl(currentTree),
);
const paths = Object.values(overrides)
.map((path) => this.normalizeLocalizedRoutePath(path))
.filter((path): path is string => !!path);
if (!paths.includes(currentPath)) {
return null;
}
return this.normalizeLocalizedRoutePath(overrides[lang]);
}
private normalizeLocalizedRouteOverrides(
paths: LocalizedRouteOverrides | null | undefined,
): LocalizedRouteOverrides | null {
if (!paths) {
return null;
}
const normalized = this.supportedLangs.reduce<LocalizedRouteOverrides>(
(accumulator, lang) => {
const path = this.normalizeLocalizedRoutePath(paths[lang]);
if (path) {
accumulator[lang] = path;
}
return accumulator;
},
{},
);
return Object.keys(normalized).length > 0 ? normalized : null;
}
private normalizeLocalizedRoutePath(
path: string | null | undefined,
): string | null {
const rawPath = String(path ?? '').trim();
if (!rawPath) {
return null;
}
const cleanPath = this.getCleanPath(rawPath);
return cleanPath.startsWith('/') ? cleanPath : null;
}
private navigateToLocalizedRoute(
currentTree: UrlTree,
localizedPath: string,
): void {
const { lang: _unusedLang, ...queryParams } = currentTree.queryParams;
const targetTree = this.router.createUrlTree(
['/', ...localizedPath.split('/').filter(Boolean)],
{
queryParams,
fragment: currentTree.fragment ?? undefined,
},
);
if (
this.router.serializeUrl(targetTree) ===
this.router.serializeUrl(currentTree)
) {
return;
}
this.router.navigateByUrl(targetTree, { replaceUrl: true });
}
private getCleanPath(url: string): string {
const path = (url || '/').split('?')[0].split('#')[0];
return path || '/';
}
private navigateIfChanged(
currentTree: UrlTree,
targetSegments: string[],

View File

@@ -29,6 +29,7 @@ describe('SeoService', () => {
data: Record<string, unknown>;
translations: Record<string, string>;
}): {
service: SeoService;
meta: jasmine.SpyObj<Meta>;
title: jasmine.SpyObj<Title>;
} {
@@ -51,7 +52,7 @@ describe('SeoService', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const service = new SeoService(router, title, meta, translate, document);
return { meta, title };
return { service, meta, title };
}
beforeEach(() => {
@@ -94,14 +95,14 @@ describe('SeoService', () => {
}));
expect(alternates).toContain({
hreflang: 'en',
hreflang: 'en-CH',
href: `${document.location.origin}/en/privacy`,
});
expect(alternates).toContain({
hreflang: 'x-default',
href: `${document.location.origin}/it/privacy`,
});
expect(document.documentElement.lang).toBe('it');
expect(document.documentElement.lang).toBe('it-CH');
const ogUrlCall = meta.updateTag.calls
.allArgs()
@@ -109,6 +110,39 @@ describe('SeoService', () => {
expect(ogUrlCall?.[0].content).toBe(
`${document.location.origin}/it/privacy`,
);
const ogLocaleCall = meta.updateTag.calls
.allArgs()
.find(([tag]) => tag.property === 'og:locale');
expect(ogLocaleCall?.[0].content).toBe('it_CH');
});
it('uses the locale-adaptive root as x-default for home pages', () => {
createService({
url: '/de',
data: {
seoTitleKey: 'SEO.ROUTES.HOME.TITLE',
seoDescriptionKey: 'SEO.ROUTES.HOME.DESCRIPTION',
},
translations: {
'SEO.ROUTES.HOME.TITLE': '3D-Druck in Zürich | 3D fab',
'SEO.ROUTES.HOME.DESCRIPTION': '3D-Druckservice in Zürich',
},
});
const alternates = Array.from(
document.head.querySelectorAll(
'link[rel="alternate"][data-seo-managed="true"]',
),
).map((node) => ({
hreflang: node.getAttribute('hreflang'),
href: node.getAttribute('href'),
}));
expect(alternates).toContain({
hreflang: 'x-default',
href: `${document.location.origin}/`,
});
});
it('resolves translated route metadata for the active language', () => {
@@ -130,6 +164,54 @@ describe('SeoService', () => {
.allArgs()
.find(([tag]) => tag.name === 'description');
expect(descriptionCall?.[0].content).toBe('About description');
expect(document.documentElement.lang).toBe('en');
expect(document.documentElement.lang).toBe('en-CH');
});
it('applies canonical and hreflang values resolved from localized paths', () => {
const { service } = createService({
url: '/it/shop/p/12345678-supporto-cavo-scrivania',
data: {},
translations: {},
});
service.applyResolvedSeo({
title: 'Supporto cavo scrivania | 3D fab',
description: 'Accessorio tecnico',
robots: 'index, follow',
ogTitle: 'Supporto cavo scrivania | 3D fab',
ogDescription: 'Accessorio tecnico',
canonicalPath: '/it/shop/p/12345678-supporto-cavo-scrivania',
alternates: {
it: '/it/shop/p/12345678-supporto-cavo-scrivania',
en: '/en/shop/p/12345678-desk-cable-clip',
de: '/de/shop/p/12345678-schreibtisch-kabelhalter',
},
xDefault: '/it/shop/p/12345678-supporto-cavo-scrivania',
});
const canonical = document.head.querySelector(
'link[rel="canonical"]',
) as HTMLLinkElement | null;
expect(canonical?.getAttribute('href')).toBe(
`${document.location.origin}/it/shop/p/12345678-supporto-cavo-scrivania`,
);
const alternates = Array.from(
document.head.querySelectorAll(
'link[rel="alternate"][data-seo-managed="true"]',
),
).map((node) => ({
hreflang: node.getAttribute('hreflang'),
href: node.getAttribute('href'),
}));
expect(alternates).toContain({
hreflang: 'de-CH',
href: `${document.location.origin}/de/shop/p/12345678-schreibtisch-kabelhalter`,
});
expect(alternates).toContain({
hreflang: 'x-default',
href: `${document.location.origin}/it/shop/p/12345678-supporto-cavo-scrivania`,
});
});
});

View File

@@ -17,7 +17,13 @@ export interface PageSeoOverride {
ogDescriptionKey?: string | null;
}
type SupportedLang = 'it' | 'en' | 'de' | 'fr';
export interface ResolvedPageSeo extends PageSeoOverride {
canonicalPath: string | null;
alternates?: SeoMap | null;
xDefault?: string | null;
}
export type SupportedLang = 'it' | 'en' | 'de' | 'fr';
type SeoMap = Partial<Record<SupportedLang, string>>;
type SeoTextDataKey =
| 'seoTitle'
@@ -51,10 +57,16 @@ export class SeoService {
this.supportedLangs,
);
private readonly ogLocaleByLang: Record<SupportedLang, string> = {
it: 'it_IT',
en: 'en_US',
de: 'de_DE',
fr: 'fr_FR',
it: 'it_CH',
en: 'en_CH',
de: 'de_CH',
fr: 'fr_CH',
};
private readonly seoLocaleByLang: Record<SupportedLang, string> = {
it: 'it-CH',
en: 'en-CH',
de: 'de-CH',
fr: 'fr-CH',
};
constructor(
@@ -79,23 +91,10 @@ export class SeoService {
applyPageSeo(override: PageSeoOverride): void {
const cleanPath = this.getCleanPath(this.router.url);
const lang = this.resolveLangFromPath(cleanPath);
const title =
this.resolveOverrideSeoText(override.title, override.titleKey) ??
this.defaultTitle(lang);
const description =
this.resolveOverrideSeoText(
override.description,
override.descriptionKey,
) ?? this.defaultDescription(lang);
const robots = this.asString(override.robots) ?? 'index, follow';
const ogTitle =
this.resolveOverrideSeoText(override.ogTitle, override.ogTitleKey) ??
title;
const ogDescription =
this.resolveOverrideSeoText(
override.ogDescription,
override.ogDescriptionKey,
) ?? description;
const { title, description, robots, ogTitle, ogDescription } =
this.resolvePageSeoOverride(override, lang);
const canonicalPath = this.buildLocalizedPath(cleanPath, lang);
const alternates = this.buildAlternatePaths(canonicalPath);
this.applySeoValues(
title,
@@ -104,6 +103,34 @@ export class SeoService {
ogTitle,
ogDescription,
cleanPath,
canonicalPath,
alternates,
this.buildXDefaultPath(canonicalPath, alternates),
lang,
);
}
applyResolvedSeo(override: ResolvedPageSeo): void {
const cleanPath = this.getCleanPath(this.router.url);
const lang = this.resolveLangFromPath(cleanPath);
const { title, description, robots, ogTitle, ogDescription } =
this.resolvePageSeoOverride(override, lang);
const canonicalPath = this.normalizeSeoPath(override.canonicalPath);
const alternates = this.normalizeAlternatePaths(override.alternates);
const xDefault =
this.normalizeSeoPath(override.xDefault) ??
this.buildXDefaultPath(canonicalPath, alternates);
this.applySeoValues(
title,
description,
robots,
ogTitle,
ogDescription,
cleanPath,
canonicalPath,
alternates,
xDefault,
lang,
);
}
@@ -122,6 +149,8 @@ export class SeoService {
const ogTitle = this.resolveSeoText(mergedData, 'ogTitle', lang) ?? title;
const ogDescription =
this.resolveSeoText(mergedData, 'ogDescription', lang) ?? description;
const canonicalPath = this.buildLocalizedPath(cleanPath, lang);
const alternates = this.buildAlternatePaths(canonicalPath);
this.applySeoValues(
title,
@@ -130,6 +159,9 @@ export class SeoService {
ogTitle,
ogDescription,
cleanPath,
canonicalPath,
alternates,
this.buildXDefaultPath(canonicalPath, alternates),
lang,
);
}
@@ -141,6 +173,9 @@ export class SeoService {
ogTitle: string,
ogDescription: string,
cleanPath: string,
canonicalPath: string | null,
alternates: SeoMap | null,
xDefaultPath: string | null,
lang: SupportedLang,
): void {
this.titleService.setTitle(title);
@@ -160,12 +195,13 @@ export class SeoService {
content: ogDescription,
});
const canonicalPath = this.buildLocalizedPath(cleanPath, lang);
const canonical = `${this.document.location.origin}${canonicalPath}`;
this.metaService.updateTag({ property: 'og:url', content: canonical });
this.updateCanonicalTag(canonical);
const ogUrl = this.toAbsoluteUrl(canonicalPath ?? cleanPath);
this.metaService.updateTag({ property: 'og:url', content: ogUrl });
this.updateCanonicalTag(
canonicalPath ? this.toAbsoluteUrl(canonicalPath) : null,
);
this.updateOpenGraphLocales(lang);
this.updateLangAndAlternates(canonicalPath, lang);
this.updateLangAndAlternates(alternates, xDefaultPath, lang);
}
private getMergedRouteData(
@@ -191,6 +227,43 @@ export class SeoService {
return this.asString(value) ?? this.resolveTranslation(key);
}
private resolvePageSeoOverride(
override: PageSeoOverride,
lang: SupportedLang,
): {
title: string;
description: string;
robots: string;
ogTitle: string;
ogDescription: string;
} {
const title =
this.resolveOverrideSeoText(override.title, override.titleKey) ??
this.defaultTitle(lang);
const description =
this.resolveOverrideSeoText(
override.description,
override.descriptionKey,
) ?? this.defaultDescription(lang);
const robots = this.asString(override.robots) ?? 'index, follow';
const ogTitle =
this.resolveOverrideSeoText(override.ogTitle, override.ogTitleKey) ??
title;
const ogDescription =
this.resolveOverrideSeoText(
override.ogDescription,
override.ogDescriptionKey,
) ?? description;
return {
title,
description,
robots,
ogTitle,
ogDescription,
};
}
private resolveSeoText(
routeData: Record<string, unknown>,
key: SeoTextDataKey,
@@ -275,10 +348,78 @@ export class SeoService {
return `/${[lang, ...segments].join('/')}`;
}
private updateCanonicalTag(url: string): void {
private buildAlternatePaths(canonicalPath: string): SeoMap {
const suffixSegments = canonicalPath.split('/').filter(Boolean).slice(1);
const suffix =
suffixSegments.length > 0 ? `/${suffixSegments.join('/')}` : '';
return this.supportedLangs.reduce<SeoMap>((accumulator, alt) => {
accumulator[alt] = `/${alt}${suffix}`;
return accumulator;
}, {});
}
private buildXDefaultPath(
canonicalPath: string | null,
alternates: SeoMap | null,
): string | null {
if (canonicalPath && this.isLocalizedHomePath(canonicalPath)) {
return '/';
}
return alternates?.it ?? canonicalPath;
}
private isLocalizedHomePath(path: string): boolean {
const segments = path.split('/').filter(Boolean);
return (
segments.length === 1 &&
this.supportedLangSet.has(segments[0] as SupportedLang)
);
}
private normalizeAlternatePaths(
paths: SeoMap | null | undefined,
): SeoMap | null {
if (!paths) {
return null;
}
const normalized = this.supportedLangs.reduce<SeoMap>(
(accumulator, lang) => {
const path = this.normalizeSeoPath(paths[lang]);
if (path) {
accumulator[lang] = path;
}
return accumulator;
},
{},
);
return Object.keys(normalized).length > 0 ? normalized : null;
}
private normalizeSeoPath(path: string | null | undefined): string | null {
const rawPath = String(path ?? '').trim();
if (!rawPath) {
return null;
}
const normalized = this.getCleanPath(rawPath);
return normalized.startsWith('/') ? normalized : null;
}
private toAbsoluteUrl(path: string): string {
return `${this.document.location.origin}${path}`;
}
private updateCanonicalTag(url: string | null): void {
let link = this.document.head.querySelector(
'link[rel="canonical"]',
) as HTMLLinkElement | null;
if (!url) {
link?.remove();
return;
}
if (!link) {
link = this.document.createElement('link');
link.setAttribute('rel', 'canonical');
@@ -308,29 +449,33 @@ export class SeoService {
}
private updateLangAndAlternates(
localizedPath: string,
alternates: SeoMap | null,
xDefaultPath: string | null,
lang: SupportedLang,
): void {
const suffixSegments = localizedPath.split('/').filter(Boolean).slice(1);
const suffix =
suffixSegments.length > 0 ? `/${suffixSegments.join('/')}` : '';
this.document.documentElement.lang = lang;
this.document.documentElement.lang = this.seoLocaleByLang[lang];
this.document.head
.querySelectorAll('link[rel="alternate"][data-seo-managed="true"]')
.forEach((node) => node.remove());
if (!alternates) {
return;
}
for (const alt of this.supportedLangs) {
const path = alternates[alt];
if (!path) {
continue;
}
this.appendAlternateLink(
alt,
`${this.document.location.origin}/${alt}${suffix}`,
this.seoLocaleByLang[alt],
this.toAbsoluteUrl(path),
);
}
this.appendAlternateLink(
'x-default',
`${this.document.location.origin}/it${suffix}`,
);
if (xDefaultPath) {
this.appendAlternateLink('x-default', this.toAbsoluteUrl(xDefaultPath));
}
}
private appendAlternateLink(hreflang: string, href: string): void {

View File

@@ -0,0 +1,28 @@
<section class="animation-test-page">
<div class="animation-toolbar" role="group" aria-label="Animation variants">
<button
type="button"
class="variant-toggle"
[class.active]="variant() === 'site-intro'"
(click)="setVariant('site-intro')"
>
Site intro
</button>
<button
type="button"
class="variant-toggle"
[class.active]="variant() === 'calculator-loader'"
(click)="setVariant('calculator-loader')"
>
Calculator loader
</button>
</div>
<div class="animation-stage" [attr.data-variant]="variant()">
<app-brand-animation-logo
[variant]="variant()"
[decorative]="false"
ariaLabel="3D fab animation test"
></app-brand-animation-logo>
</div>
</section>

View File

@@ -0,0 +1,60 @@
:host {
display: block;
}
.animation-test-page {
min-height: 100vh;
display: grid;
align-content: center;
justify-items: center;
gap: 1.5rem;
padding: 2rem 1.5rem 3rem;
background: #fff;
}
.animation-toolbar {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem;
border: 1px solid rgba(16, 24, 32, 0.12);
border-radius: 999px;
background: #f7f5ef;
}
.variant-toggle {
min-height: 2.4rem;
padding: 0 1rem;
border: 0;
border-radius: 999px;
background: transparent;
color: var(--color-text-muted);
font: inherit;
font-weight: 600;
cursor: pointer;
transition:
background-color 0.18s ease,
color 0.18s ease,
box-shadow 0.18s ease;
}
.variant-toggle.active {
background: #fff;
color: var(--color-text);
box-shadow: 0 6px 16px rgba(16, 24, 32, 0.08);
}
.animation-stage {
width: min(100%, 26rem);
}
@media (max-width: 640px) {
.animation-toolbar {
flex-wrap: wrap;
justify-content: center;
}
.animation-stage {
width: min(100%, 19rem);
}
}

View File

@@ -0,0 +1,21 @@
import { CommonModule } from '@angular/common';
import { Component, signal } from '@angular/core';
import {
BrandAnimationLogoComponent,
BrandAnimationVariant,
} from '../../shared/components/brand-animation-logo/brand-animation-logo.component';
@Component({
selector: 'app-calculator-animation-test',
standalone: true,
imports: [CommonModule, BrandAnimationLogoComponent],
templateUrl: './calculator-animation-test.component.html',
styleUrl: './calculator-animation-test.component.scss',
})
export class CalculatorAnimationTestComponent {
readonly variant = signal<BrandAnimationVariant>('site-intro');
setVariant(variant: BrandAnimationVariant): void {
this.variant.set(variant);
}
}

View File

@@ -57,7 +57,10 @@
@if (loading()) {
<app-card class="loading-state">
<div class="loader-content">
<div class="spinner"></div>
<app-brand-animation-logo
class="loader-logo"
variant="calculator-loader"
></app-brand-animation-logo>
<h3 class="loading-title">
{{ "CALC.ANALYZING_TITLE" | translate }}
</h3>

View File

@@ -93,7 +93,7 @@
.loader-content {
text-align: center;
max-width: 300px;
max-width: 22rem;
margin: 0 auto;
/* Center content vertically within the stretched card */
@@ -101,12 +101,14 @@
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: var(--space-3);
}
.loading-title {
font-size: 1.1rem;
font-weight: 600;
margin: var(--space-4) 0 var(--space-2);
margin: 0;
color: var(--color-text);
}
@@ -114,23 +116,21 @@
font-size: 0.9rem;
color: var(--color-text-muted);
line-height: 1.5;
margin: 0;
}
.spinner {
border: 3px solid var(--color-neutral-200);
border-left-color: var(--color-brand);
border-radius: 50%;
width: 48px;
height: 48px;
animation: spin 1s linear infinite;
.loader-logo {
display: block;
width: min(100%, 16rem);
margin: 0 auto;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
--brand-animation-width: 16rem;
--brand-animation-height: 4.8rem;
--brand-animation-letter-width: 2.85rem;
--brand-animation-scale: 0.84;
--brand-animation-word-spacing: 0.97;
--brand-animation-width-mobile: 14rem;
--brand-animation-height-mobile: 4.1rem;
--brand-animation-letter-width-mobile: 2.45rem;
--brand-animation-scale-mobile: 0.84;
--brand-animation-loader-loop-duration: 2.65s;
}

View File

@@ -17,6 +17,7 @@ import { catchError, map } from 'rxjs/operators';
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
import { AppAlertComponent } from '../../shared/components/app-alert/app-alert.component';
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
import { BrandAnimationLogoComponent } from '../../shared/components/brand-animation-logo/brand-animation-logo.component';
import { UploadFormComponent } from './components/upload-form/upload-form.component';
import { QuoteResultComponent } from './components/quote-result/quote-result.component';
import {
@@ -48,6 +49,7 @@ type TrackedPrintSettings = {
AppCardComponent,
AppAlertComponent,
AppButtonComponent,
BrandAnimationLogoComponent,
UploadFormComponent,
QuoteResultComponent,
SuccessStateComponent,

View File

@@ -3,6 +3,18 @@ import { CalculatorPageComponent } from './calculator-page.component';
export const CALCULATOR_ROUTES: Routes = [
{ path: '', redirectTo: 'basic', pathMatch: 'full' },
{
path: 'animation-test',
loadComponent: () =>
import('./calculator-animation-test.component').then(
(m) => m.CalculatorAnimationTestComponent,
),
data: {
seoTitleKey: 'SEO.ROUTES.CALCULATOR.TITLE',
seoDescriptionKey: 'SEO.ROUTES.CALCULATOR.DESCRIPTION',
seoRobots: 'noindex, nofollow',
},
},
{
path: 'basic',
component: CalculatorPageComponent,

View File

@@ -127,7 +127,8 @@ export class UploadFormComponent implements OnInit {
private layerHeightsByNozzle: Record<string, SimpleOption[]> = {};
private isPatchingSettings = false;
acceptedFormats = '.stl,.3mf,.step,.stp';
acceptedFormats = '.stl,.3mf';
private readonly allowedExtensions = ['stl', '3mf'] as const;
constructor() {
this.form = this.fb.group({
@@ -286,6 +287,13 @@ export class UploadFormComponent implements OnInit {
return name.endsWith('.stl');
}
isSupportedFile(file: File | null): boolean {
if (!file) return false;
const name = file.name.toLowerCase().trim();
return this.allowedExtensions.some((ext) => name.endsWith(`.${ext}`));
}
canPreviewSelectedFile(): boolean {
return this.isStlFile(this.getSelectedPreviewFile());
}
@@ -340,13 +348,19 @@ export class UploadFormComponent implements OnInit {
onFilesDropped(newFiles: File[]) {
const MAX_SIZE = 200 * 1024 * 1024;
const validItems: FormItem[] = [];
let hasError = false;
let hasInvalidType = false;
let hasOversize = false;
const defaults = this.getCurrentGlobalItemDefaults();
for (const file of newFiles) {
if (!this.isSupportedFile(file)) {
hasInvalidType = true;
continue;
}
if (file.size > MAX_SIZE) {
hasError = true;
hasOversize = true;
continue;
}
@@ -367,7 +381,11 @@ export class UploadFormComponent implements OnInit {
});
}
if (hasError) {
if (hasInvalidType) {
alert(this.translate.instant('CALC.ERR_INVALID_FILE_TYPE'));
}
if (hasOversize) {
alert(this.translate.instant('CALC.ERR_FILE_TOO_LARGE'));
}

View File

@@ -1,5 +1,10 @@
<article class="product-card">
<a class="media" [routerLink]="productLink()" [state]="navigationState()">
<a
class="media"
[routerLink]="productLink()"
[state]="navigationState()"
(click)="rememberCatalogScroll()"
>
@if (imageUrl(); as imageUrl) {
<img
[src]="imageUrl"
@@ -32,9 +37,12 @@
</div>
<h3 class="name">
<a [routerLink]="productLink()" [state]="navigationState()">{{
product().name
}}</a>
<a
[routerLink]="productLink()"
[state]="navigationState()"
(click)="rememberCatalogScroll()"
>{{ product().name }}</a
>
</h3>
<p class="excerpt">
@@ -62,6 +70,7 @@
<a
[routerLink]="productLink()"
[state]="navigationState()"
(click)="rememberCatalogScroll()"
class="view-btn"
>{{ "SHOP.DETAILS" | translate }}</a
>

View File

@@ -74,4 +74,16 @@ export class ProductCardComponent {
shopReturnUrl: this.router.url,
};
}
rememberCatalogScroll(): void {
if (typeof window === 'undefined') {
return;
}
const nextState = {
...(history.state ?? {}),
shopRestoreScrollY: Math.max(0, Math.round(window.scrollY)),
};
history.replaceState(nextState, '');
}
}

View File

@@ -4,7 +4,7 @@
← {{ "SHOP.BACK" | translate }}
</button>
@if (loading()) {
@if (loading() || softFallbackActive()) {
<div class="detail-grid skeleton-grid">
<div class="skeleton-block"></div>
<div class="skeleton-block"></div>

View File

@@ -0,0 +1,272 @@
import { Location } from '@angular/common';
import { PLATFORM_ID, RESPONSE_INIT, signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { of } from 'rxjs';
import { SeoService } from '../../core/services/seo.service';
import { LanguageService } from '../../core/services/language.service';
import { ShopRouteService } from './services/shop-route.service';
import { ShopProductDetail, ShopService } from './services/shop.service';
import { ProductDetailComponent } from './product-detail.component';
describe('ProductDetailComponent', () => {
function buildProduct(
overrides: Partial<ShopProductDetail> = {},
): ShopProductDetail {
return {
id: '91823f84-1111-2222-3333-444444444444',
slug: 'bike-wall-hanger',
name: 'Bike Wall-Hanger',
excerpt: 'Wall mount for bicycles',
description: '<p>Wall mount for bicycles</p>',
seoTitle: null,
seoDescription: null,
ogTitle: null,
ogDescription: null,
indexable: true,
isFeatured: false,
sortOrder: 0,
category: {
id: 'category-1',
slug: 'bike-accessories',
name: 'Bike Accessories',
},
breadcrumbs: [],
priceFromChf: 29.9,
priceToChf: 29.9,
defaultVariant: {
id: 'variant-1',
sku: 'BW-1',
variantLabel: 'PLA',
colorName: 'Black',
colorLabel: 'Black',
colorHex: '#111111',
priceChf: 29.9,
isDefault: true,
},
variants: [
{
id: 'variant-1',
sku: 'BW-1',
variantLabel: 'PLA',
colorName: 'Black',
colorLabel: 'Black',
colorHex: '#111111',
priceChf: 29.9,
isDefault: true,
},
],
primaryImage: null,
images: [],
model3d: null,
publicPath: '91823f84-bike-wall-hanger',
localizedPaths: {
it: '/it/shop/p/91823f84-supporto-bici-muro',
en: '/en/shop/p/91823f84-bike-wall-hanger',
de: '/de/shop/p/91823f84-bike-wall-hanger',
fr: '/fr/shop/p/91823f84-support-mural-velo',
},
...overrides,
};
}
function createComponent(
routerUrl = '/de/shop/p/91823f84-bike-wall-hanger',
options?: {
currentLang?: 'it' | 'en' | 'de' | 'fr';
selectedLang?: 'it' | 'en' | 'de' | 'fr';
},
) {
const responseInit: { status?: number } = {};
const seoService = jasmine.createSpyObj<SeoService>('SeoService', [
'applyResolvedSeo',
'applyPageSeo',
]);
const translate = jasmine.createSpyObj<TranslateService>(
'TranslateService',
['instant'],
);
translate.instant.and.callFake((key: string) => {
const translations: Record<string, string> = {
'SHOP.TITLE': 'Technische Lösungen',
'SHOP.CATALOG_META_DESCRIPTION':
'Entdecken Sie technische 3D-Druck-Lösungen.',
'SEO.ROUTES.SHOP.PRODUCT_TITLE': 'Produkt | 3D fab',
'SEO.ROUTES.SHOP.PRODUCT_DESCRIPTION':
'Entdecken Sie Details, Materialien, Varianten und Verfügbarkeit.',
};
return translations[key] ?? key;
});
const currentLang = signal<'it' | 'en' | 'de' | 'fr'>(
options?.currentLang ?? 'de',
);
const languageService = {
currentLang,
selectedLang: () => options?.selectedLang ?? currentLang(),
setLocalizedRouteOverrides: jasmine.createSpy(
'setLocalizedRouteOverrides',
),
clearLocalizedRouteOverrides: jasmine.createSpy(
'clearLocalizedRouteOverrides',
),
};
const shopService = {
cartLoaded: signal(false),
cartLoading: signal(false),
getProductByPublicPath: jasmine
.createSpy('getProductByPublicPath')
.and.returnValue(of(buildProduct())),
quantityForVariant: jasmine
.createSpy('quantityForVariant')
.and.returnValue(0),
loadCart: jasmine.createSpy('loadCart').and.returnValue(of(null)),
resolveMediaUrl: jasmine
.createSpy('resolveMediaUrl')
.and.returnValue(null),
};
const router = {
url: routerUrl,
navigate: jasmine.createSpy('navigate'),
navigateByUrl: jasmine.createSpy('navigateByUrl'),
parseUrl: jasmine.createSpy('parseUrl'),
createUrlTree: jasmine.createSpy('createUrlTree'),
serializeUrl: jasmine.createSpy('serializeUrl'),
} as unknown as Router;
const activatedRoute = {
paramMap: of(
convertToParamMap({ productSlug: '91823f84-bike-wall-hanger' }),
),
snapshot: {
paramMap: convertToParamMap({
productSlug: '91823f84-bike-wall-hanger',
}),
},
} as unknown as ActivatedRoute;
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [ProductDetailComponent],
providers: [
{ provide: SeoService, useValue: seoService },
{ provide: TranslateService, useValue: translate },
{ provide: LanguageService, useValue: languageService },
{ provide: ShopService, useValue: shopService },
{
provide: ShopRouteService,
useValue: jasmine.createSpyObj<ShopRouteService>('ShopRouteService', [
'shopRootCommands',
'productPathSegment',
'isCatalogUrl',
]),
},
{ provide: Router, useValue: router },
{ provide: ActivatedRoute, useValue: activatedRoute },
{
provide: Location,
useValue: jasmine.createSpyObj<Location>('Location', ['back']),
},
{ provide: RESPONSE_INIT, useValue: responseInit },
{ provide: PLATFORM_ID, useValue: 'server' },
],
});
const fixture: ComponentFixture<ProductDetailComponent> =
TestBed.createComponent(ProductDetailComponent);
return {
component: fixture.componentInstance,
seoService,
responseInit,
};
}
it('applies index follow SEO for indexable products', () => {
const { component, seoService } = createComponent();
(component as any).applySeo(buildProduct());
expect(seoService.applyResolvedSeo).toHaveBeenCalledWith(
jasmine.objectContaining({
title: 'Bike Wall-Hanger | 3D fab',
robots: 'index, follow',
canonicalPath: '/de/shop/p/91823f84-bike-wall-hanger',
alternates: buildProduct().localizedPaths,
xDefault: '/it/shop/p/91823f84-supporto-bici-muro',
}),
);
});
it('uses the route language for canonical SEO even if the selected translation language lags', () => {
const { component, seoService } = createComponent(undefined, {
currentLang: 'de',
selectedLang: 'en',
});
(component as any).applySeo(buildProduct());
expect(seoService.applyResolvedSeo).toHaveBeenCalledWith(
jasmine.objectContaining({
canonicalPath: '/de/shop/p/91823f84-bike-wall-hanger',
}),
);
});
it('applies noindex for products explicitly marked as non-indexable', () => {
const { component, seoService } = createComponent();
(component as any).applySeo(buildProduct({ indexable: false }));
expect(seoService.applyResolvedSeo).toHaveBeenCalledWith(
jasmine.objectContaining({
robots: 'noindex, nofollow',
}),
);
});
it('builds a soft SSR fallback with 200 + index follow', () => {
const { component, seoService, responseInit } = createComponent();
expect(
(component as any).shouldUseSoftSeoFallback({ status: 500 }),
).toBeTrue();
(component as any).setResponseStatus(200);
(component as any).applySoftFallbackSeo('91823f84-bike-wall-hanger');
expect(responseInit.status).toBe(200);
expect(seoService.applyResolvedSeo).toHaveBeenCalledWith(
jasmine.objectContaining({
title: 'Bike Wall Hanger | 3D fab',
description:
'Entdecken Sie Details, Materialien, Varianten und Verfügbarkeit.',
robots: 'index, follow',
canonicalPath: '/de/shop/p/91823f84-bike-wall-hanger',
alternates: null,
xDefault: null,
}),
);
});
it('keeps hard fallback noindex for missing products', () => {
const { component, seoService, responseInit } = createComponent();
expect(
(component as any).shouldUseSoftSeoFallback({ status: 404 }),
).toBeFalse();
(component as any).setResponseStatus(404);
(component as any).applyHardFallbackSeo();
expect(responseInit.status).toBe(404);
expect(seoService.applyResolvedSeo).toHaveBeenCalledWith(
jasmine.objectContaining({
robots: 'noindex, nofollow',
alternates: null,
xDefault: null,
}),
);
});
});

View File

@@ -1,5 +1,6 @@
import { CommonModule, isPlatformBrowser } from '@angular/common';
import { CommonModule, Location, isPlatformBrowser } from '@angular/common';
import {
RESPONSE_INIT,
afterNextRender,
Component,
DestroyRef,
@@ -7,16 +8,24 @@ import {
PLATFORM_ID,
computed,
inject,
input,
signal,
} from '@angular/core';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import { Router, RouterLink } from '@angular/router';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { catchError, combineLatest, finalize, of, switchMap, tap } from 'rxjs';
import {
catchError,
combineLatest,
distinctUntilChanged,
finalize,
map,
of,
switchMap,
tap,
} from 'rxjs';
import { SeoService } from '../../core/services/seo.service';
import { LanguageService } from '../../core/services/language.service';
import { findColorHex, getColorHex } from '../../core/constants/colors.const';
import { findColorHex } from '../../core/constants/colors.const';
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component';
@@ -26,6 +35,7 @@ import {
ShopService,
} from './services/shop.service';
import { ShopRouteService } from './services/shop-route.service';
import { humanizeShopSlug } from './shop-seo-fallback';
interface ShopMaterialOption {
key: string;
@@ -59,18 +69,23 @@ export class ProductDetailComponent {
/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
private readonly destroyRef = inject(DestroyRef);
private readonly injector = inject(Injector);
private readonly location = inject(Location);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly translate = inject(TranslateService);
private readonly seoService = inject(SeoService);
private readonly languageService = inject(LanguageService);
private readonly shopRouteService = inject(ShopRouteService);
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
private readonly responseInit = inject(RESPONSE_INIT, { optional: true });
readonly shopService = inject(ShopService);
readonly categorySlug = input<string | undefined>();
readonly productSlug = input<string | undefined>();
readonly routeCategorySlug = signal<string | null>(
this.readRouteParam('categorySlug'),
);
readonly loading = signal(true);
readonly softFallbackActive = signal(false);
readonly error = signal<string | null>(null);
readonly product = signal<ShopProductDetail | null>(null);
readonly selectedVariantId = signal<string | null>(null);
@@ -197,39 +212,73 @@ export class ProductDetailComponent {
afterNextRender(() => {
this.scheduleCartWarmup();
});
this.destroyRef.onDestroy(() => {
this.languageService.clearLocalizedRouteOverrides();
});
combineLatest([
toObservable(this.productSlug, { injector: this.injector }),
this.route.paramMap.pipe(
map((params) => ({
categorySlug: this.normalizeRouteParam(params.get('categorySlug')),
productSlug: this.normalizeRouteParam(params.get('productSlug')),
})),
distinctUntilChanged(
(previous, current) =>
previous.categorySlug === current.categorySlug &&
previous.productSlug === current.productSlug,
),
),
toObservable(this.languageService.currentLang, {
injector: this.injector,
}),
}).pipe(distinctUntilChanged()),
])
.pipe(
tap(() => {
this.loading.set(true);
this.softFallbackActive.set(false);
this.error.set(null);
this.addSuccess.set(false);
this.modelError.set(false);
this.colorPopupOpen.set(false);
this.modelModalOpen.set(false);
}),
switchMap(([productSlug]) => {
if (!productSlug) {
switchMap(([routeParams]) => {
this.routeCategorySlug.set(routeParams.categorySlug);
if (!routeParams.productSlug) {
this.languageService.clearLocalizedRouteOverrides();
this.error.set('SHOP.NOT_FOUND');
this.setResponseStatus(404);
this.applyHardFallbackSeo();
this.loading.set(false);
return of(null);
}
const productSlug = routeParams.productSlug as string;
return this.shopService.getProductByPublicPath(productSlug).pipe(
catchError((error) => {
this.languageService.clearLocalizedRouteOverrides();
this.product.set(null);
this.selectedVariantId.set(null);
this.setSelectedImageAssetId(null);
this.modelFile.set(null);
this.error.set(
error?.status === 404 ? 'SHOP.NOT_FOUND' : 'SHOP.LOAD_ERROR',
);
this.applyFallbackSeo();
const isNotFound = error?.status === 404;
if (isNotFound) {
this.error.set('SHOP.NOT_FOUND');
this.setResponseStatus(404);
this.applyHardFallbackSeo();
return of(null);
}
if (this.shouldUseSoftSeoFallback(error)) {
this.error.set(null);
this.softFallbackActive.set(true);
this.setResponseStatus(200);
this.applySoftFallbackSeo(productSlug);
return of(null);
}
this.error.set('SHOP.LOAD_ERROR');
this.setResponseStatus(503);
return of(null);
}),
finalize(() => this.loading.set(false)),
@@ -243,6 +292,7 @@ export class ProductDetailComponent {
}
this.product.set(product);
this.softFallbackActive.set(false);
this.selectedVariantId.set(
product.defaultVariant?.id ?? product.variants[0]?.id ?? null,
);
@@ -257,6 +307,7 @@ export class ProductDetailComponent {
null,
);
this.quantity.set(1);
this.languageService.setLocalizedRouteOverrides(product.localizedPaths);
this.syncPublicUrl(product);
this.applySeo(product);
this.modelFile.set(null);
@@ -273,7 +324,7 @@ export class ProductDetailComponent {
this.shopService.resolveMediaUrl(image.hero) ??
this.shopService.resolveMediaUrl(image.card) ??
this.shopService.resolveMediaUrl(image.thumb)
);
);
}
private scheduleCartWarmup(): void {
@@ -478,7 +529,8 @@ export class ProductDetailComponent {
}
productLinkRoot(): string[] {
const categorySlug = this.product()?.category.slug || this.categorySlug();
const categorySlug =
this.product()?.category.slug || this.routeCategorySlug();
return this.shopRouteService.shopRootCommands(categorySlug);
}
@@ -489,6 +541,11 @@ export class ProductDetailComponent {
: null;
if (returnUrl && this.shopRouteService.isCatalogUrl(returnUrl)) {
if (this.isBrowser && window.history.length > 1) {
this.location.back();
return;
}
void this.router.navigateByUrl(returnUrl);
return;
}
@@ -548,28 +605,86 @@ export class ProductDetailComponent {
this.translate.instant('SHOP.CATALOG_META_DESCRIPTION');
const robots =
product.indexable === false ? 'noindex, nofollow' : 'index, follow';
const lang = this.languageService.currentLang();
const canonicalPath =
product.localizedPaths?.[lang] ?? product.localizedPaths?.it ?? null;
this.seoService.applyPageSeo({
this.seoService.applyResolvedSeo({
title,
description,
robots,
ogTitle: product.ogTitle || title,
ogDescription: product.ogDescription || description,
canonicalPath,
alternates: product.localizedPaths,
xDefault: product.localizedPaths?.it ?? canonicalPath,
});
}
private applyFallbackSeo(): void {
private applyHardFallbackSeo(): void {
const title = `${this.translate.instant('SHOP.TITLE')} | 3D fab`;
const description = this.translate.instant('SHOP.CATALOG_META_DESCRIPTION');
this.seoService.applyPageSeo({
this.seoService.applyResolvedSeo({
title,
description,
robots: 'noindex, nofollow',
ogTitle: title,
ogDescription: description,
canonicalPath: null,
alternates: null,
xDefault: null,
});
}
private applySoftFallbackSeo(productSlug: string): void {
const title = this.buildSoftFallbackTitle(productSlug);
const description = this.resolveTranslatedText(
'SEO.ROUTES.SHOP.PRODUCT_DESCRIPTION',
this.translate.instant('SHOP.CATALOG_META_DESCRIPTION'),
);
this.seoService.applyResolvedSeo({
title,
description,
robots: 'index, follow',
ogTitle: title,
ogDescription: description,
canonicalPath: this.currentPath(),
alternates: null,
xDefault: null,
});
}
private shouldUseSoftSeoFallback(error: { status?: number } | null): boolean {
return !this.isBrowser && error?.status !== 404;
}
private buildSoftFallbackTitle(productSlug: string): string {
const humanized = humanizeShopSlug(productSlug, {
stripProductIdPrefix: true,
});
if (humanized) {
return `${humanized} | 3D fab`;
}
return this.resolveTranslatedText(
'SEO.ROUTES.SHOP.PRODUCT_TITLE',
`${this.translate.instant('SHOP.TITLE')} | 3D fab`,
);
}
private resolveTranslatedText(key: string, fallback: string): string {
const translated = this.translate.instant(key);
return typeof translated === 'string' && translated !== key
? translated
: fallback;
}
private currentPath(): string {
const path = String(this.router.url ?? '/').split(/[?#]/, 1)[0] || '/';
return path.startsWith('/') ? path : `/${path}`;
}
private materialLabelForVariant(
variant: ShopProductVariantOption | null,
): string {
@@ -741,21 +856,23 @@ export class ProductDetailComponent {
return;
}
const currentProductSlug = this.productSlug()?.trim().toLowerCase() ?? '';
const targetProductSlug = this.shopRouteService.productPathSegment(product);
if (currentProductSlug === targetProductSlug) {
const currentTree = this.router.parseUrl(this.router.url);
const lang = this.languageService.currentLang();
const targetPath =
product.localizedPaths?.[lang] ??
`/${lang}/shop/p/${this.shopRouteService.productPathSegment(product)}`;
const normalizedTargetPath = targetPath.startsWith('/')
? targetPath
: `/${targetPath}`;
const currentPath = this.router
.serializeUrl(currentTree)
.split(/[?#]/, 1)[0];
if (currentPath === normalizedTargetPath) {
return;
}
const currentTree = this.router.parseUrl(this.router.url);
const targetTree = this.router.createUrlTree(
[
'/',
this.languageService.selectedLang(),
'shop',
'p',
targetProductSlug,
],
['/', ...normalizedTargetPath.split('/').filter(Boolean)],
{
queryParams: currentTree.queryParams,
fragment: currentTree.fragment ?? undefined,
@@ -774,4 +891,19 @@ export class ProductDetailComponent {
state: history.state,
});
}
private setResponseStatus(status: number): void {
if (this.responseInit) {
this.responseInit.status = status;
}
}
private readRouteParam(name: string): string | null {
return this.normalizeRouteParam(this.route.snapshot.paramMap.get(name));
}
private normalizeRouteParam(value: string | null | undefined): string | null {
const normalized = String(value ?? '').trim();
return normalized || null;
}
}

View File

@@ -1,15 +1,14 @@
import { Injectable } from '@angular/core';
import { LanguageService } from '../../../core/services/language.service';
type SupportedLang = 'it' | 'en' | 'de' | 'fr';
export interface ShopProductRouteRef {
id: string | null | undefined;
name: string | null | undefined;
slug?: string | null | undefined;
}
export interface ShopProductLookup {
idPrefix: string | null;
slugHint: string | null;
publicPath?: string | null | undefined;
localizedPaths?: Partial<Record<SupportedLang, string>> | null | undefined;
}
@Injectable({
@@ -26,11 +25,21 @@ export class ShopRouteService {
}
productCommands(product: ShopProductRouteRef): string[] {
const localizedPath = this.localizedProductPath(product);
if (localizedPath) {
return ['/', ...localizedPath.split('/').filter(Boolean)];
}
const lang = this.languageService.currentLang();
return ['/', lang, 'shop', 'p', this.productPathSegment(product)];
}
productPathSegment(product: ShopProductRouteRef): string {
const publicPath = String(product.publicPath ?? '').trim();
if (publicPath) {
return publicPath;
}
const idPrefix = this.productIdPrefix(product.id);
const tail =
this.slugify(product.name) || this.slugify(product.slug) || 'product';
@@ -38,41 +47,6 @@ export class ShopRouteService {
return idPrefix ? `${idPrefix}-${tail}` : tail;
}
resolveProductLookup(
productPathSegment: string | null | undefined,
): ShopProductLookup {
const normalized = String(productPathSegment ?? '')
.trim()
.toLowerCase();
if (!normalized) {
return {
idPrefix: null,
slugHint: null,
};
}
const bareUuidMatch = normalized.match(/^([0-9a-f]{8})$/);
if (bareUuidMatch) {
return {
idPrefix: bareUuidMatch[1],
slugHint: null,
};
}
const publicSlugMatch = normalized.match(/^([0-9a-f]{8})-(.+)$/);
if (publicSlugMatch) {
return {
idPrefix: publicSlugMatch[1],
slugHint: this.slugify(publicSlugMatch[2]) || null,
};
}
return {
idPrefix: null,
slugHint: normalized,
};
}
isCatalogUrl(url: string | null | undefined): boolean {
if (!url) {
return false;
@@ -92,6 +66,12 @@ export class ShopRouteService {
.replace(/-{2,}/g, '-');
}
private localizedProductPath(product: ShopProductRouteRef): string | null {
const lang = this.languageService.currentLang();
const localizedPath = String(product.localizedPaths?.[lang] ?? '').trim();
return localizedPath.startsWith('/') ? localizedPath : null;
}
private productIdPrefix(productId: string | null | undefined): string {
const normalized = String(productId ?? '')
.trim()

View File

@@ -1,3 +1,4 @@
import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
HttpClientTestingModule,
@@ -5,7 +6,6 @@ import {
} from '@angular/common/http/testing';
import {
ShopCartResponse,
ShopProductCatalogResponse,
ShopProductDetail,
ShopService,
} from './shop.service';
@@ -14,6 +14,11 @@ import { LanguageService } from '../../../core/services/language.service';
describe('ShopService', () => {
let service: ShopService;
let httpMock: HttpTestingController;
const currentLang = signal<'it' | 'en' | 'de' | 'fr'>('it');
const languageService = {
currentLang,
selectedLang: jasmine.createSpy('selectedLang').and.returnValue('it'),
};
const buildCart = (): ShopCartResponse => ({
session: {
@@ -90,32 +95,6 @@ describe('ShopService', () => {
grandTotalChf: 36.8,
});
const buildCatalog = (): ShopProductCatalogResponse => ({
categorySlug: null,
featuredOnly: false,
category: null,
products: [
{
id: '12345678-abcd-4abc-9abc-1234567890ab',
slug: 'desk-cable-clip',
name: 'Supporto cavo scrivania',
excerpt: 'Accessorio tecnico',
isFeatured: true,
sortOrder: 0,
category: {
id: 'category-1',
slug: 'accessori',
name: 'Accessori',
},
priceFromChf: 9.9,
priceToChf: 12.5,
defaultVariant: null,
primaryImage: null,
model3d: null,
},
],
});
const buildProduct = (): ShopProductDetail => ({
id: '12345678-abcd-4abc-9abc-1234567890ab',
slug: 'desk-cable-clip',
@@ -142,6 +121,13 @@ describe('ShopService', () => {
primaryImage: null,
images: [],
model3d: null,
publicPath: '12345678-supporto-cavo-scrivania',
localizedPaths: {
it: '/it/shop/p/12345678-supporto-cavo-scrivania',
en: '/en/shop/p/12345678-desk-cable-clip',
de: '/de/shop/p/12345678-schreibtisch-kabelhalter',
fr: '/fr/shop/p/12345678-support-cable-bureau',
},
});
beforeEach(() => {
@@ -151,13 +137,14 @@ describe('ShopService', () => {
ShopService,
{
provide: LanguageService,
useValue: {
selectedLang: () => 'it',
},
useValue: languageService,
},
],
});
currentLang.set('it');
languageService.selectedLang.and.returnValue('it');
service = TestBed.inject(ShopService);
httpMock = TestBed.inject(HttpTestingController);
});
@@ -212,85 +199,89 @@ describe('ShopService', () => {
response = product;
});
const catalogRequest = httpMock.expectOne((request) => {
return (
request.method === 'GET' &&
request.url === 'http://localhost:8000/api/shop/products' &&
request.params.get('lang') === 'it'
);
});
catalogRequest.flush(buildCatalog());
const detailRequest = httpMock.expectOne((request) => {
const request = httpMock.expectOne((request) => {
return (
request.method === 'GET' &&
request.url ===
'http://localhost:8000/api/shop/products/desk-cable-clip' &&
'http://localhost:8000/api/shop/products/by-id-prefix/12345678' &&
request.params.get('lang') === 'it'
);
});
detailRequest.flush(buildProduct());
request.flush(buildProduct());
expect(response?.id).toBe('12345678-abcd-4abc-9abc-1234567890ab');
expect(response?.name).toBe('Supporto cavo scrivania');
});
it('resolves product detail from uuid prefix even when slug tail does not match', () => {
it('resolves products from the stable uuid prefix even if the slug tail is stale', () => {
let response: ShopProductDetail | undefined;
service.getProductByPublicPath('12345678-qualunque-nome').subscribe({
next: (product) => {
response = product;
},
error: () =>
fail('Expected stale slug tails to resolve from the uuid prefix'),
});
const request = httpMock.expectOne((request) => {
return (
request.method === 'GET' &&
request.url ===
'http://localhost:8000/api/shop/products/by-id-prefix/12345678' &&
request.params.get('lang') === 'it'
);
});
request.flush(buildProduct());
expect(response?.id).toBe('12345678-abcd-4abc-9abc-1234567890ab');
});
it('resolves bare uuid product paths through the stable uuid prefix endpoint', () => {
let response: ShopProductDetail | undefined;
service.getProductByPublicPath('12345678').subscribe({
next: (product) => {
response = product;
},
error: () =>
fail('Expected bare uuid path to resolve from the uuid prefix'),
});
const request = httpMock.expectOne((request) => {
return (
request.method === 'GET' &&
request.url ===
'http://localhost:8000/api/shop/products/by-id-prefix/12345678' &&
request.params.get('lang') === 'it'
);
});
request.flush(buildProduct());
expect(response?.publicPath).toBe('12345678-supporto-cavo-scrivania');
});
it('uses the route language for public shop lookups when translate.currentLang lags behind', () => {
let response: ShopProductDetail | undefined;
currentLang.set('de');
languageService.selectedLang.and.returnValue('en');
service
.getProductByPublicPath('12345678-qualunque-nome')
.getProductByPublicPath('12345678-schreibtisch-kabelhalter')
.subscribe((product) => {
response = product;
});
const catalogRequest = httpMock.expectOne((request) => {
return (
request.method === 'GET' &&
request.url === 'http://localhost:8000/api/shop/products' &&
request.params.get('lang') === 'it'
);
});
catalogRequest.flush(buildCatalog());
const detailRequest = httpMock.expectOne((request) => {
const request = httpMock.expectOne((request) => {
return (
request.method === 'GET' &&
request.url ===
'http://localhost:8000/api/shop/products/desk-cable-clip' &&
request.params.get('lang') === 'it'
'http://localhost:8000/api/shop/products/by-id-prefix/12345678' &&
request.params.get('lang') === 'de'
);
});
detailRequest.flush(buildProduct());
expect(response?.id).toBe('12345678-abcd-4abc-9abc-1234567890ab');
});
it('resolves product detail from bare uuid prefix without slug tail', () => {
let response: ShopProductDetail | undefined;
service.getProductByPublicPath('12345678').subscribe((product) => {
response = product;
});
const catalogRequest = httpMock.expectOne((request) => {
return (
request.method === 'GET' &&
request.url === 'http://localhost:8000/api/shop/products' &&
request.params.get('lang') === 'it'
);
});
catalogRequest.flush(buildCatalog());
const detailRequest = httpMock.expectOne((request) => {
return (
request.method === 'GET' &&
request.url ===
'http://localhost:8000/api/shop/products/desk-cable-clip' &&
request.params.get('lang') === 'it'
);
});
detailRequest.flush(buildProduct());
request.flush(buildProduct());
expect(response?.id).toBe('12345678-abcd-4abc-9abc-1234567890ab');
});

View File

@@ -1,13 +1,15 @@
import { computed, inject, Injectable, signal } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { map, Observable, switchMap, tap, throwError } from 'rxjs';
import { map, Observable, tap, throwError } from 'rxjs';
import { environment } from '../../../../environments/environment';
import {
PublicMediaUsageDto,
PublicMediaVariantDto,
} from '../../../core/services/public-media.service';
import { LanguageService } from '../../../core/services/language.service';
import { ShopRouteService } from './shop-route.service';
type SupportedLang = 'it' | 'en' | 'de' | 'fr';
type LocalizedPathMap = Partial<Record<SupportedLang, string>>;
export interface ShopCategoryRef {
id: string;
@@ -84,6 +86,8 @@ export interface ShopProductSummary {
defaultVariant: ShopProductVariantOption | null;
primaryImage: PublicMediaUsageDto | null;
model3d: ShopProductModel | null;
publicPath: string;
localizedPaths: LocalizedPathMap;
}
export interface ShopProductDetail {
@@ -108,6 +112,8 @@ export interface ShopProductDetail {
primaryImage: PublicMediaUsageDto | null;
images: PublicMediaUsageDto[];
model3d: ShopProductModel | null;
publicPath: string;
localizedPaths: LocalizedPathMap;
}
export interface ShopProductCatalogResponse {
@@ -185,7 +191,6 @@ export interface ShopCategoryNavNode {
export class ShopService {
private readonly http = inject(HttpClient);
private readonly languageService = inject(LanguageService);
private readonly shopRouteService = inject(ShopRouteService);
private readonly apiUrl = `${environment.apiUrl}/api/shop`;
readonly cart = signal<ShopCartResponse | null>(null);
@@ -278,27 +283,32 @@ export class ShopService {
getProductByPublicPath(
productPathSegment: string,
): Observable<ShopProductDetail> {
const lookup =
this.shopRouteService.resolveProductLookup(productPathSegment);
if (!lookup.idPrefix && lookup.slugHint) {
return this.getProduct(lookup.slugHint);
const normalizedPath = this.normalizePublicPath(productPathSegment);
if (!normalizedPath) {
return throwError(() => ({
status: 404,
}));
}
return this.getProductCatalog().pipe(
map((catalog) =>
catalog.products.find((product) =>
product.id.toLowerCase().startsWith(lookup.idPrefix ?? ''),
),
),
switchMap((product) => {
if (!product) {
return throwError(() => ({
status: 404,
}));
}
return this.getProduct(product.slug);
}),
);
const productIdPrefix = this.extractProductIdPrefix(normalizedPath);
const endpoint = productIdPrefix
? `${this.apiUrl}/products/by-id-prefix/${encodeURIComponent(productIdPrefix)}`
: `${this.apiUrl}/products/by-path/${encodeURIComponent(normalizedPath)}`;
return this.http.get<ShopProductDetail>(endpoint, {
params: this.buildLangParams(),
});
}
private normalizePublicPath(value: string | null | undefined): string {
return String(value ?? '')
.trim()
.toLowerCase();
}
private extractProductIdPrefix(value: string): string | null {
const match = value.match(/^([0-9a-f]{8})(?:-|$)/);
return match?.[1] ?? null;
}
loadCart(): Observable<ShopCartResponse> {
@@ -452,7 +462,10 @@ export class ShopService {
}
private buildLangParams(): HttpParams {
return new HttpParams().set('lang', this.languageService.selectedLang());
// Public shop URLs are localized. During direct loads the translation
// service can still momentarily reflect the browser language, while the
// route language has already been resolved from the URL.
return new HttpParams().set('lang', this.languageService.currentLang());
}
private setCart(cart: ShopCartResponse): void {

View File

@@ -1,15 +1,7 @@
<section class="shop-page">
<div class="container ui-simple-hero shop-hero">
<h1 class="ui-simple-hero__title">{{ "NAV.SHOP" | translate }}</h1>
<p class="ui-simple-hero__subtitle">
{{
selectedCategory()
? selectedCategory()?.description ||
("SHOP.CATEGORY_META"
| translate: { count: selectedCategory()?.productCount || 0 })
: ("SHOP.SUBTITLE" | translate)
}}
</p>
<p class="ui-simple-hero__subtitle">{{ heroSubtitle() }}</p>
</div>
<div class="container shop-layout">
@@ -181,17 +173,9 @@
<div class="section-head catalog-head">
<div>
<p class="ui-eyebrow ui-eyebrow--compact">
{{
selectedCategory()
? ("SHOP.SELECTED_CATEGORY" | translate)
: ("SHOP.CATALOG_LABEL" | translate)
}}
{{ catalogEyebrow() }}
</p>
<h2 class="section-title">
{{
selectedCategory()?.name || ("SHOP.CATALOG_TITLE" | translate)
}}
</h2>
<h2 class="section-title">{{ catalogTitle() }}</h2>
</div>
<span class="catalog-counter">
{{ products().length }}
@@ -199,7 +183,7 @@
</span>
</div>
@if (loading()) {
@if (loading() || softFallbackActive()) {
<div class="product-grid skeleton-grid">
@for (ghost of [1, 2, 3, 4]; track ghost) {
<div class="skeleton-card"></div>

View File

@@ -332,6 +332,10 @@
}
@media (max-width: 760px) {
.cart-card {
display: none;
}
.product-grid {
grid-template-columns: 1fr;
}

View File

@@ -0,0 +1,233 @@
import { PLATFORM_ID, RESPONSE_INIT, signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { of } from 'rxjs';
import { SeoService } from '../../core/services/seo.service';
import { LanguageService } from '../../core/services/language.service';
import {
ShopCategoryDetail,
ShopCategoryTree,
ShopProductCatalogResponse,
ShopService,
} from './services/shop.service';
import { ShopRouteService } from './services/shop-route.service';
import { ShopPageComponent } from './shop-page.component';
describe('ShopPageComponent', () => {
function buildCategory(
overrides: Partial<ShopCategoryDetail> = {},
): ShopCategoryDetail {
return {
id: 'cat-1',
slug: 'compatible-with-garmin',
name: 'Compatible with Garmin',
description: 'Accessories compatible with Garmin devices.',
seoTitle: null,
seoDescription: null,
ogTitle: null,
ogDescription: null,
indexable: true,
sortOrder: 0,
productCount: 3,
breadcrumbs: [],
primaryImage: null,
images: [],
children: [],
...overrides,
};
}
function buildCatalog(
overrides: Partial<ShopProductCatalogResponse> = {},
): ShopProductCatalogResponse {
return {
categorySlug: null,
featuredOnly: null,
category: null,
products: [],
...overrides,
};
}
function createComponent(routerUrl = '/de/shop') {
const responseInit: { status?: number } = {};
const seoService = jasmine.createSpyObj<SeoService>('SeoService', [
'applyResolvedSeo',
'applyPageSeo',
]);
const translate = jasmine.createSpyObj<TranslateService>(
'TranslateService',
['instant'],
);
translate.instant.and.callFake(
(key: string, params?: { count?: number }) => {
const translations: Record<string, string> = {
'SHOP.TITLE': 'Technische Lösungen',
'SHOP.SUBTITLE': 'Fertige Produkte, die praktische Probleme lösen',
'SHOP.CATALOG_TITLE': 'Alle Produkte',
'SHOP.CATALOG_LABEL': 'Katalog',
'SHOP.SELECTED_CATEGORY': 'Ausgewählte Kategorie',
'SHOP.CATALOG_META_DESCRIPTION':
'Entdecken Sie 3D-gedruckte Produkte und technisches Zubehör.',
'SEO.ROUTES.SHOP.CATEGORY_TITLE': 'Shop-Kategorie | 3D fab',
'SEO.ROUTES.SHOP.CATEGORY_DESCRIPTION':
'Entdecken Sie Produkte dieser Kategorie und technische Lösungen.',
};
if (key === 'SHOP.CATEGORY_META') {
return `${params?.count ?? 0} Produkte in dieser Kategorie verfügbar`;
}
return translations[key] ?? key;
},
);
const currentLang = signal<'it' | 'en' | 'de' | 'fr'>('de');
const languageService = {
currentLang,
selectedLang: () => currentLang(),
};
const shopService = {
cart: signal(null),
cartLoading: signal(false),
cartLoaded: signal(false),
cartItemCount: signal(0),
cartSessionId: signal<string | null>(null),
getCategories: jasmine
.createSpy('getCategories')
.and.returnValue(of([] as ShopCategoryTree[])),
getProductCatalog: jasmine
.createSpy('getProductCatalog')
.and.returnValue(of(buildCatalog())),
flattenCategoryTree: jasmine
.createSpy('flattenCategoryTree')
.and.returnValue([]),
quantityForProduct: jasmine
.createSpy('quantityForProduct')
.and.returnValue(0),
loadCart: jasmine.createSpy('loadCart').and.returnValue(of(null)),
clearCart: jasmine.createSpy('clearCart').and.returnValue(of(null)),
removeCartItem: jasmine
.createSpy('removeCartItem')
.and.returnValue(of(null)),
updateCartItem: jasmine
.createSpy('updateCartItem')
.and.returnValue(of(null)),
};
const router = {
url: routerUrl,
navigate: jasmine.createSpy('navigate'),
} as unknown as Router;
const activatedRoute = {
paramMap: of(convertToParamMap({})),
snapshot: {
paramMap: convertToParamMap({}),
},
} as unknown as ActivatedRoute;
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [ShopPageComponent],
providers: [
{ provide: SeoService, useValue: seoService },
{ provide: TranslateService, useValue: translate },
{ provide: LanguageService, useValue: languageService },
{ provide: ShopService, useValue: shopService },
{
provide: ShopRouteService,
useValue: jasmine.createSpyObj<ShopRouteService>('ShopRouteService', [
'shopRootCommands',
]),
},
{ provide: Router, useValue: router },
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: RESPONSE_INIT, useValue: responseInit },
{ provide: PLATFORM_ID, useValue: 'server' },
],
});
const fixture: ComponentFixture<ShopPageComponent> =
TestBed.createComponent(ShopPageComponent);
return {
component: fixture.componentInstance,
seoService,
responseInit,
};
}
it('keeps index follow on the public shop root', () => {
const { component, seoService } = createComponent();
(component as any).applyDefaultSeo();
expect(seoService.applyPageSeo).toHaveBeenCalledWith(
jasmine.objectContaining({
title: 'Technische Lösungen | 3D fab',
robots: 'index, follow',
}),
);
});
it('keeps noindex for categories explicitly marked as non-indexable', () => {
const { component, seoService } = createComponent(
'/de/shop/compatible-with-garmin',
);
(component as any).applySeo(buildCategory({ indexable: false }));
expect(seoService.applyPageSeo).toHaveBeenCalledWith(
jasmine.objectContaining({
robots: 'noindex, nofollow',
}),
);
});
it('uses a soft SSR fallback for non-404 category load errors', () => {
const { component, seoService, responseInit } = createComponent(
'/de/shop/compatible-with-garmin',
);
expect(
(component as any).shouldUseSoftSeoFallback({ status: 500 }),
).toBeTrue();
(component as any).setResponseStatus(200);
(component as any).applySoftFallbackSeo('compatible-with-garmin');
expect(responseInit.status).toBe(200);
expect(seoService.applyResolvedSeo).toHaveBeenCalledWith(
jasmine.objectContaining({
title: 'Compatible With Garmin | Technische Lösungen | 3D fab',
description:
'Entdecken Sie Produkte dieser Kategorie und technische Lösungen.',
robots: 'index, follow',
canonicalPath: '/de/shop/compatible-with-garmin',
alternates: null,
xDefault: null,
}),
);
});
it('keeps hard 404 noindex behavior for missing categories', () => {
const { component, seoService, responseInit } = createComponent(
'/de/shop/compatible-with-garmin',
);
expect(
(component as any).shouldUseSoftSeoFallback({ status: 404 }),
).toBeFalse();
(component as any).setResponseStatus(404);
(component as any).applyHardErrorSeo();
expect(responseInit.status).toBe(404);
expect(seoService.applyResolvedSeo).toHaveBeenCalledWith(
jasmine.objectContaining({
robots: 'noindex, nofollow',
alternates: null,
xDefault: null,
}),
);
});
});

View File

@@ -1,22 +1,25 @@
import { CommonModule } from '@angular/common';
import { CommonModule, isPlatformBrowser } from '@angular/common';
import {
PLATFORM_ID,
RESPONSE_INIT,
afterNextRender,
Component,
DestroyRef,
Injector,
computed,
inject,
input,
signal,
} from '@angular/core';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import { Router, RouterLink } from '@angular/router';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import {
catchError,
combineLatest,
distinctUntilChanged,
finalize,
forkJoin,
map,
of,
switchMap,
tap,
@@ -39,6 +42,7 @@ import {
ShopService,
} from './services/shop.service';
import { ShopRouteService } from './services/shop-route.service';
import { humanizeShopSlug } from './shop-seo-fallback';
@Component({
selector: 'app-shop-page',
@@ -57,16 +61,23 @@ import { ShopRouteService } from './services/shop-route.service';
export class ShopPageComponent {
private readonly destroyRef = inject(DestroyRef);
private readonly injector = inject(Injector);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly translate = inject(TranslateService);
private readonly seoService = inject(SeoService);
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
private readonly responseInit = inject(RESPONSE_INIT, { optional: true });
readonly languageService = inject(LanguageService);
private readonly shopRouteService = inject(ShopRouteService);
readonly shopService = inject(ShopService);
readonly categorySlug = input<string | undefined>();
readonly routeCategorySlug = signal<string | null>(
this.readRouteParam('categorySlug'),
);
readonly loading = signal(true);
readonly softFallbackActive = signal(false);
readonly softFallbackCategoryLabel = signal<string | null>(null);
readonly error = signal<string | null>(null);
readonly categories = signal<ShopCategoryTree[]>([]);
readonly categoryNodes = signal<ShopCategoryNavNode[]>([]);
@@ -80,7 +91,7 @@ export class ShopPageComponent {
readonly cartLoading = this.shopService.cartLoading;
readonly cartItemCount = this.shopService.cartItemCount;
readonly currentCategorySlug = computed(
() => this.selectedCategory()?.slug ?? this.categorySlug() ?? null,
() => this.selectedCategory()?.slug ?? this.routeCategorySlug() ?? null,
);
readonly cartItems = computed(() =>
(this.cart()?.items ?? []).filter(
@@ -88,6 +99,44 @@ export class ShopPageComponent {
),
);
readonly cartHasItems = computed(() => this.cartItems().length > 0);
readonly heroSubtitle = computed(() => {
this.languageService.currentLang();
const category = this.selectedCategory();
if (category) {
return (
category.description ||
this.translate.instant('SHOP.CATEGORY_META', {
count: category.productCount || 0,
})
);
}
if (this.softFallbackActive() && this.routeCategorySlug()) {
return this.resolveTranslatedText(
'SEO.ROUTES.SHOP.CATEGORY_DESCRIPTION',
this.translate.instant('SHOP.CATALOG_META_DESCRIPTION'),
);
}
return this.translate.instant('SHOP.SUBTITLE');
});
readonly catalogEyebrow = computed(() => {
this.languageService.currentLang();
return this.selectedCategory() || this.softFallbackCategoryLabel()
? this.translate.instant('SHOP.SELECTED_CATEGORY')
: this.translate.instant('SHOP.CATALOG_LABEL');
});
readonly catalogTitle = computed(() => {
this.languageService.currentLang();
return (
this.selectedCategory()?.name ||
this.softFallbackCategoryLabel() ||
this.translate.instant('SHOP.CATALOG_TITLE')
);
});
constructor() {
afterNextRender(() => {
@@ -95,35 +144,58 @@ export class ShopPageComponent {
});
combineLatest([
toObservable(this.categorySlug, { injector: this.injector }),
this.route.paramMap.pipe(
map((params) => this.normalizeRouteParam(params.get('categorySlug'))),
distinctUntilChanged(),
),
toObservable(this.languageService.currentLang, {
injector: this.injector,
}),
}).pipe(distinctUntilChanged()),
])
.pipe(
tap(() => {
this.loading.set(true);
this.softFallbackActive.set(false);
this.softFallbackCategoryLabel.set(null);
this.error.set(null);
}),
switchMap(([categorySlug]) =>
forkJoin({
switchMap(([categorySlug]) => {
this.routeCategorySlug.set(categorySlug);
return forkJoin({
categories: this.shopService.getCategories(),
catalog: this.shopService.getProductCatalog(categorySlug ?? null),
}).pipe(
catchError((error) => {
const isNotFound = error?.status === 404;
this.categories.set([]);
this.categoryNodes.set([]);
this.selectedCategory.set(null);
this.products.set([]);
this.error.set(
error?.status === 404 ? 'SHOP.NOT_FOUND' : 'SHOP.LOAD_ERROR',
);
this.applyDefaultSeo();
if (isNotFound) {
this.error.set('SHOP.NOT_FOUND');
this.setResponseStatus(404);
this.applyHardErrorSeo();
return of(null);
}
if (this.shouldUseSoftSeoFallback(error)) {
this.error.set(null);
this.softFallbackActive.set(true);
this.softFallbackCategoryLabel.set(
categorySlug ? humanizeShopSlug(categorySlug) : null,
);
this.setResponseStatus(200);
this.applySoftFallbackSeo(categorySlug);
return of(null);
}
this.error.set('SHOP.LOAD_ERROR');
this.setResponseStatus(503);
return of(null);
}),
finalize(() => this.loading.set(false)),
),
),
);
}),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((result) => {
@@ -135,12 +207,15 @@ export class ShopPageComponent {
this.categoryNodes.set(
this.shopService.flattenCategoryTree(
result.categories,
result.catalog.category?.slug ?? this.categorySlug() ?? null,
result.catalog.category?.slug ?? this.routeCategorySlug() ?? null,
),
);
this.selectedCategory.set(result.catalog.category ?? null);
this.products.set(result.catalog.products);
this.softFallbackActive.set(false);
this.softFallbackCategoryLabel.set(null);
this.applySeo(result.catalog.category ?? null);
this.restoreCatalogScrollIfNeeded();
});
}
@@ -353,4 +428,108 @@ export class ShopPageComponent {
ogDescription: description,
});
}
private applyHardErrorSeo(): void {
const title = `${this.translate.instant('SHOP.TITLE')} | 3D fab`;
const description = this.translate.instant('SHOP.CATALOG_META_DESCRIPTION');
this.seoService.applyResolvedSeo({
title,
description,
robots: 'noindex, nofollow',
ogTitle: title,
ogDescription: description,
canonicalPath: null,
alternates: null,
xDefault: null,
});
}
private applySoftFallbackSeo(categorySlug: string | null): void {
if (!categorySlug) {
this.applyDefaultSeo();
return;
}
const title = this.buildSoftFallbackCategoryTitle(categorySlug);
const description = this.resolveTranslatedText(
'SEO.ROUTES.SHOP.CATEGORY_DESCRIPTION',
this.translate.instant('SHOP.CATALOG_META_DESCRIPTION'),
);
this.seoService.applyResolvedSeo({
title,
description,
robots: 'index, follow',
ogTitle: title,
ogDescription: description,
canonicalPath: this.currentPath(),
alternates: null,
xDefault: null,
});
}
private shouldUseSoftSeoFallback(error: { status?: number } | null): boolean {
return !this.isBrowser && error?.status !== 404;
}
private buildSoftFallbackCategoryTitle(categorySlug: string): string {
const shopTitle = this.translate.instant('SHOP.TITLE');
const humanized = humanizeShopSlug(categorySlug);
if (humanized) {
return `${humanized} | ${shopTitle} | 3D fab`;
}
return this.resolveTranslatedText(
'SEO.ROUTES.SHOP.CATEGORY_TITLE',
`${shopTitle} | 3D fab`,
);
}
private resolveTranslatedText(key: string, fallback: string): string {
const translated = this.translate.instant(key);
return typeof translated === 'string' && translated !== key
? translated
: fallback;
}
private currentPath(): string {
const path = String(this.router.url ?? '/').split(/[?#]/, 1)[0] || '/';
return path.startsWith('/') ? path : `/${path}`;
}
private setResponseStatus(status: number): void {
if (this.responseInit) {
this.responseInit.status = status;
}
}
private restoreCatalogScrollIfNeeded(): void {
if (typeof window === 'undefined') {
return;
}
const scrollY = Number(history.state?.shopRestoreScrollY);
if (!Number.isFinite(scrollY) || scrollY < 0) {
return;
}
const { shopRestoreScrollY: _ignored, ...nextState } = history.state ?? {};
const restore = () => window.scrollTo({ left: 0, top: scrollY });
history.replaceState(nextState, '');
window.requestAnimationFrame(() => {
restore();
window.setTimeout(restore, 60);
});
}
private readRouteParam(name: string): string | null {
return this.normalizeRouteParam(this.route.snapshot.paramMap.get(name));
}
private normalizeRouteParam(value: string | null | undefined): string | null {
const normalized = String(value ?? '').trim();
return normalized || null;
}
}

View File

@@ -0,0 +1,72 @@
const PRODUCT_ID_PREFIX_PATTERN = /^[0-9a-f]{8}-(?=[a-z0-9])/i;
const UPPERCASE_TOKENS = new Set([
'3d',
'abs',
'asa',
'cad',
'cf',
'gf',
'pa',
'pc',
'petg',
'pla',
'pp',
'tpu',
'uv',
]);
export function humanizeShopSlug(
value: string | null | undefined,
options?: {
stripProductIdPrefix?: boolean;
},
): string {
const normalized = normalizeShopSlug(value, options?.stripProductIdPrefix);
if (!normalized) {
return '';
}
return normalized
.split('-')
.filter(Boolean)
.map(formatSlugToken)
.join(' ')
.trim();
}
function normalizeShopSlug(
value: string | null | undefined,
stripProductIdPrefix = false,
): string {
const normalized = String(value ?? '')
.trim()
.replace(/^\/+|\/+$/g, '')
.split('/')
.filter(Boolean)
.at(-1)
?.toLowerCase();
if (!normalized) {
return '';
}
return stripProductIdPrefix
? normalized.replace(PRODUCT_ID_PREFIX_PATTERN, '')
: normalized;
}
function formatSlugToken(token: string): string {
if (!token) {
return '';
}
if (/^\d+$/.test(token)) {
return token;
}
if (UPPERCASE_TOKENS.has(token)) {
return token.toUpperCase();
}
return `${token.charAt(0).toUpperCase()}${token.slice(1)}`;
}

View File

@@ -12,7 +12,7 @@ import { TranslateModule } from '@ngx-translate/core';
export class AppDropzoneComponent {
label = input<string>('DROPZONE.DEFAULT_LABEL');
subtext = input<string>('DROPZONE.DEFAULT_SUBTEXT');
accept = input<string>('.stl,.3mf,.step,.stp');
accept = input<string>('.stl,.3mf');
multiple = input<boolean>(true);
filesDropped = output<File[]>();

View File

@@ -0,0 +1,17 @@
<div
class="brand-animation"
[attr.data-variant]="variant()"
role="img"
[attr.aria-hidden]="decorative() ? 'true' : null"
[attr.aria-label]="decorative() ? null : ariaLabel()"
>
@for (letter of resolvedLetters(); track letter.key) {
<img
class="brand-animation__letter"
[src]="letter.src"
alt=""
[attr.data-letter]="letter.key"
[style.--word-x]="letter.wordX"
/>
}
</div>

View File

@@ -0,0 +1,172 @@
:host {
display: block;
width: 100%;
}
.brand-animation {
--three-anchor-x: -9.4rem;
--bee-anchor-x: 10.2rem;
--word-scale: var(--brand-animation-scale, 1);
--word-spacing-factor: var(--brand-animation-word-spacing, 1);
--loader-group-scale-x: 0.94;
--loader-group-scale-y: 1.05;
--loader-exit-shift: 1.6rem;
position: relative;
width: min(100%, var(--brand-animation-width, 26rem));
height: var(--brand-animation-height, 8rem);
margin-inline: auto;
}
.brand-animation__letter {
--word-x: 0rem;
position: absolute;
top: 50%;
left: 50%;
width: var(--brand-animation-letter-width, clamp(2.7rem, 6vw, 4rem));
height: auto;
transform: translate(-50%, -50%);
transform-origin: center center;
will-change: transform;
}
.brand-animation[data-variant="site-intro"] .brand-animation__letter {
animation: site-intro-preview var(--brand-animation-site-intro-duration, 1s)
linear 1 forwards;
}
.brand-animation[data-variant="calculator-loader"] .brand-animation__letter {
animation: calculator-loader-loop
var(--brand-animation-loader-loop-duration, 2.65s) infinite;
}
@keyframes site-intro-preview {
0% {
transform: translate(-50%, -50%) translateX(0) scale(0.92);
}
20% {
transform: translate(-50%, -50%) translateX(0) scale(0.92);
}
80% {
transform: translate(-50%, -50%)
translateX(
calc(var(--word-x) * var(--word-scale) * var(--word-spacing-factor))
)
scale(1);
}
100% {
transform: translate(-50%, -50%)
translateX(
calc(var(--word-x) * var(--word-scale) * var(--word-spacing-factor))
)
scale(1);
}
}
@keyframes calculator-loader-loop {
0%,
5% {
opacity: 0;
transform: translate(-50%, -50%)
translateX(
calc(
var(--three-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
)
)
scaleX(var(--loader-group-scale-x)) scaleY(var(--loader-group-scale-y));
}
12% {
opacity: 1;
transform: translate(-50%, -50%)
translateX(
calc(
var(--three-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
)
)
scaleX(var(--loader-group-scale-x)) scaleY(var(--loader-group-scale-y));
}
12% {
animation-timing-function: cubic-bezier(0.22, 0.82, 0.28, 1);
}
38%,
56% {
opacity: 1;
transform: translate(-50%, -50%)
translateX(
calc(var(--word-x) * var(--word-scale) * var(--word-spacing-factor))
)
scale(1);
}
56% {
animation-timing-function: cubic-bezier(0.38, 0, 0.72, 1);
}
82%,
88% {
opacity: 1;
transform: translate(-50%, -50%)
translateX(
calc(
var(--bee-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
)
)
scaleX(var(--loader-group-scale-x)) scaleY(var(--loader-group-scale-y));
}
88% {
animation-timing-function: cubic-bezier(0.4, 0, 1, 1);
}
94% {
opacity: 0;
transform: translate(-50%, -50%)
translateX(
calc(
(var(--bee-anchor-x) + var(--loader-exit-shift)) * var(--word-scale) *
var(--word-spacing-factor)
)
)
scaleX(0.98) scaleY(1.02);
}
94.01%,
100% {
opacity: 0;
transform: translate(-50%, -50%)
translateX(
calc(
var(--three-anchor-x) * var(--word-scale) * var(--word-spacing-factor)
)
)
scaleX(var(--loader-group-scale-x)) scaleY(var(--loader-group-scale-y));
}
}
@media (max-width: 640px) {
.brand-animation {
width: min(100%, var(--brand-animation-width-mobile, 19rem));
height: var(--brand-animation-height-mobile, 6rem);
--word-scale: var(--brand-animation-scale-mobile, 0.74);
}
.brand-animation__letter {
width: var(--brand-animation-letter-width-mobile, 2.8rem);
}
}
@media (prefers-reduced-motion: reduce) {
.brand-animation__letter {
animation: none !important;
transform: translate(-50%, -50%)
translateX(
calc(var(--word-x) * var(--word-scale) * var(--word-spacing-factor))
)
scale(1);
}
}

View File

@@ -0,0 +1,69 @@
import { Component, computed, input } from '@angular/core';
export type BrandAnimationVariant = 'site-intro' | 'calculator-loader';
interface AnimationLetter {
key: string;
darkSrc: string;
yellowSrc: string;
wordX: string;
}
interface ResolvedAnimationLetter {
key: string;
src: string;
wordX: string;
}
const LETTERS: readonly AnimationLetter[] = [
{
key: '3',
darkSrc: '/assets/images/animation/31200.svg',
yellowSrc: '/assets/images/animation/3g1200.svg',
wordX: '-9.4rem',
},
{
key: 'd',
darkSrc: '/assets/images/animation/d1200.svg',
yellowSrc: '/assets/images/animation/Dg1200.svg',
wordX: '-4.9rem',
},
{
key: 'F',
darkSrc: '/assets/images/animation/F1200.svg',
yellowSrc: '/assets/images/animation/Fg1200.svg',
wordX: '1rem',
},
{
key: 'A',
darkSrc: '/assets/images/animation/A1200.svg',
yellowSrc: '/assets/images/animation/Ag1200.svg',
wordX: '5.6rem',
},
{
key: 'B',
darkSrc: '/assets/images/animation/B1200.svg',
yellowSrc: '/assets/images/animation/Bg1200.svg',
wordX: '10.2rem',
},
] as const;
@Component({
selector: 'app-brand-animation-logo',
standalone: true,
templateUrl: './brand-animation-logo.component.html',
styleUrl: './brand-animation-logo.component.scss',
})
export class BrandAnimationLogoComponent {
readonly variant = input<BrandAnimationVariant>('site-intro');
readonly decorative = input(true);
readonly ariaLabel = input('3D fab animated logo');
readonly resolvedLetters = computed<ResolvedAnimationLetter[]>(() =>
LETTERS.map((letter) => ({
key: letter.key,
src: this.variant() === 'site-intro' ? letter.yellowSrc : letter.darkSrc,
wordX: letter.wordX,
})),
);
}

View File

@@ -107,14 +107,14 @@
},
"CALC": {
"TITLE": "3D-Angebot berechnen",
"SUBTITLE": "Laden Sie Ihre 3D-Datei (STL, 3MF, STEP) hoch, stellen Sie Qualität und Farbe ein und berechnen Sie sofort Preis und Lieferzeit.",
"SUBTITLE": "Laden Sie Ihre 3D-Datei (STL, 3MF) hoch, stellen Sie Qualität und Farbe ein und berechnen Sie sofort Preis und Lieferzeit.",
"CTA_START": "Jetzt starten",
"BUSINESS": "Unternehmen",
"PRIVATE": "Privat",
"MODE_EASY": "Basis",
"MODE_ADVANCED": "Erweitert",
"UPLOAD_LABEL": "Ziehen Sie Ihre 3D-Datei hierher",
"UPLOAD_SUB": "Wir unterstützen STL, 3MF, STEP bis 50MB",
"UPLOAD_SUB": "Wir unterstützen STL, 3MF bis 50MB",
"MATERIAL": "Material",
"QUALITY": "Qualität",
"QUANTITY": "Menge",
@@ -141,11 +141,12 @@
"BENEFITS_2": "Ausgewählte Materialien und Qualitätskontrolle",
"BENEFITS_3": "CAD-Beratung, falls die Datei Änderungen benötigt",
"ERR_FILE_REQUIRED": "Die Datei ist erforderlich.",
"STEP_WARNING": "Die 3D-Ansicht ist mit STEP- und 3MF-Dateien nicht kompatibel",
"STEP_WARNING": "Die 3D-Vorschau ist nur für STL-Dateien verfügbar.",
"REMOVE_FILE": "Datei entfernen",
"FALLBACK_MATERIAL": "PLA (Fallback)",
"FALLBACK_QUALITY_STANDARD": "Standard",
"ERR_FILE_TOO_LARGE": "Einige Dateien überschreiten das 200MB-Limit und wurden nicht hinzugefügt.",
"ERR_INVALID_FILE_TYPE": "Sie können nur Dateien vom Typ .stl oder .3mf hochladen.",
"PRINT_SPEED": "Druckgeschwindigkeit",
"COLOR": "Farbe",
"ANALYZING_TITLE": "Analyse läuft...",
@@ -612,11 +613,11 @@
"HERO_TITLE": "3D-Druckservice.<br>Von der Datei zum fertigen Teil.",
"HERO_LEAD": "Mit dem fortschrittlichsten Rechner für Ihre 3D-Drucke: absolute Präzision und keine Überraschungen.",
"HERO_SUBTITLE": "Wir bieten auch CAD-Services für individuelle Teile an!",
"HERO_SWISS_TITLE": "Based in Switzerland",
"HERO_SWISS_TITLE": "Mit Sitz in der Schweiz",
"HERO_SWISS_COPY": "Produktion und Support in der Schweiz.",
"HERO_SWISS_LOCATIONS_LABEL": "Standorte",
"HERO_SWISS_LOCATION_1": "Ticino",
"HERO_SWISS_LOCATION_2": "Zurich",
"HERO_SWISS_LOCATION_2": "Zürich",
"HERO_SWISS_LOCATION_3": "Biel/Bienne",
"HERO_SWISS_NOTE": "In der ganzen Schweiz aktiv.",
"BTN_CALCULATE": "Angebot berechnen",
@@ -624,7 +625,7 @@
"BTN_CONTACT": "Mit uns sprechen",
"SEC_CALC_TITLE": "Korrekte Preisberechnung in wenigen Sekunden",
"SEC_CALC_SUBTITLE": "Keine Registrierung erforderlich. Die Schätzung basiert auf echtem Slicing.",
"SEC_CALC_LIST_1": "Unterstützte Formate: STL, 3MF, STEP",
"SEC_CALC_LIST_1": "Unterstützte Formate: STL, 3MF",
"CARD_CALC_EYEBROW": "Automatische Berechnung",
"CARD_CALC_TITLE": "Preis und Lieferzeit mit einem Klick",
"CARD_CALC_TAG": "Ohne Registrierung",
@@ -674,7 +675,7 @@
},
"DROPZONE": {
"DEFAULT_LABEL": "Dateien hierher ziehen oder klicken zum Hochladen",
"DEFAULT_SUBTEXT": "Unterstützt .STL, .3MF, .STEP"
"DEFAULT_SUBTEXT": "Unterstützt .STL, .3MF"
},
"COLOR": {
"AVAILABLE_COLORS": "Verfügbare Farben",

View File

@@ -107,14 +107,14 @@
},
"CALC": {
"TITLE": "3D Print Calculator",
"SUBTITLE": "Upload your 3D file (STL, 3MF, STEP...) and get an instant estimate of costs and print time.",
"SUBTITLE": "Upload your 3D file (STL, 3MF) and get an instant estimate of costs and print time.",
"CTA_START": "Start Now",
"BUSINESS": "Business",
"PRIVATE": "Private",
"MODE_EASY": "Quick",
"MODE_ADVANCED": "Advanced",
"UPLOAD_LABEL": "Drag your 3D file here",
"UPLOAD_SUB": "Supports STL, 3MF, STEP up to 50MB",
"UPLOAD_SUB": "Supports STL, 3MF up to 50MB",
"MATERIAL": "Material",
"QUALITY": "Quality",
"QUANTITY": "Quantity",
@@ -141,11 +141,12 @@
"BENEFITS_2": "Selected materials and quality control",
"BENEFITS_3": "CAD consultation if file needs modifications",
"ERR_FILE_REQUIRED": "File is required.",
"STEP_WARNING": "3D preview is not available for STEP files, but the calculator works perfectly. You can proceed with the quotation.",
"STEP_WARNING": "3D preview is available only for STL files.",
"REMOVE_FILE": "Remove file",
"FALLBACK_MATERIAL": "PLA (fallback)",
"FALLBACK_QUALITY_STANDARD": "Standard",
"ERR_FILE_TOO_LARGE": "Some files exceed the 200MB limit and were not added.",
"ERR_INVALID_FILE_TYPE": "You can upload only .stl or .3mf files.",
"PRINT_SPEED": "Print speed",
"COLOR": "Color",
"ANALYZING_TITLE": "Analysis in progress...",
@@ -624,7 +625,7 @@
"BTN_CONTACT": "Talk to us",
"SEC_CALC_TITLE": "Accurate pricing in a few seconds",
"SEC_CALC_SUBTITLE": "No registration required. The estimate is calculated through real slicing.",
"SEC_CALC_LIST_1": "Supported formats: STL, 3MF, STEP",
"SEC_CALC_LIST_1": "Supported formats: STL, 3MF",
"CARD_CALC_EYEBROW": "Automatic calculation",
"CARD_CALC_TITLE": "Price and lead time in one click",
"CARD_CALC_TAG": "No registration",
@@ -674,7 +675,7 @@
},
"DROPZONE": {
"DEFAULT_LABEL": "Drop files here or click to upload",
"DEFAULT_SUBTEXT": "Supports .stl, .3mf, .step"
"DEFAULT_SUBTEXT": "Supports .stl, .3mf"
},
"COLOR": {
"AVAILABLE_COLORS": "Available colors",

View File

@@ -84,7 +84,7 @@
"HERO_TITLE": "Service d'impression 3D.<br>Du fichier à la pièce finie.",
"HERO_LEAD": "Avec le calculateur le plus avancé pour vos impressions 3D : précision absolue et zéro surprise.",
"HERO_SUBTITLE": "Nous proposons aussi des services CAD pour des pièces personnalisées !",
"HERO_SWISS_TITLE": "Based in Switzerland",
"HERO_SWISS_TITLE": "Basés en Suisse",
"HERO_SWISS_COPY": "Production et support en Suisse.",
"HERO_SWISS_LOCATIONS_LABEL": "Sites",
"HERO_SWISS_LOCATION_1": "Ticino",
@@ -96,7 +96,7 @@
"BTN_CONTACT": "Parlez avec nous",
"SEC_CALC_TITLE": "Prix correct en quelques secondes",
"SEC_CALC_SUBTITLE": "Aucune inscription requise. L'estimation est effectuée via un vrai slicing.",
"SEC_CALC_LIST_1": "Formats pris en charge : STL, 3MF, STEP",
"SEC_CALC_LIST_1": "Formats pris en charge : STL, 3MF",
"CARD_CALC_EYEBROW": "Calcul automatique",
"CARD_CALC_TITLE": "Prix et délais en un clic",
"CARD_CALC_TAG": "Sans inscription",
@@ -139,14 +139,14 @@
},
"CALC": {
"TITLE": "Calculer un devis 3D",
"SUBTITLE": "Chargez votre fichier 3D (STL, 3MF, STEP), réglez la qualité et la couleur puis calculez immédiatement prix et délais.",
"SUBTITLE": "Chargez votre fichier 3D (STL, 3MF), réglez la qualité et la couleur puis calculez immédiatement prix et délais.",
"CTA_START": "Commencer maintenant",
"BUSINESS": "Entreprises",
"PRIVATE": "Particuliers",
"MODE_EASY": "Base",
"MODE_ADVANCED": "Avancée",
"UPLOAD_LABEL": "Glissez votre fichier 3D ici",
"UPLOAD_SUB": "Nous prenons en charge STL, 3MF, STEP jusqu'à 50MB",
"UPLOAD_SUB": "Nous prenons en charge STL, 3MF jusqu'à 50MB",
"MATERIAL": "Matériau",
"QUALITY": "Qualité",
"PRINT_SPEED": "Vitesse d'impression",
@@ -185,11 +185,12 @@
"NOTES_PLACEHOLDER": "Instructions spécifiques...",
"SETUP_NOTE": "* Inclut {{cost}} comme coût de setup",
"SHIPPING_NOTE": "* Frais d'expédition exclus, calculés à l'étape suivante",
"STEP_WARNING": "La visualisation 3D n'est pas compatible avec les fichiers STEP et 3MF",
"STEP_WARNING": "La prévisualisation 3D est disponible uniquement pour les fichiers STL.",
"REMOVE_FILE": "Supprimer le fichier",
"FALLBACK_MATERIAL": "PLA (fallback)",
"FALLBACK_QUALITY_STANDARD": "Standard",
"ERR_FILE_TOO_LARGE": "Certains fichiers dépassent la limite de 200MB et n'ont pas été ajoutés.",
"ERR_INVALID_FILE_TYPE": "Vous pouvez téléverser uniquement des fichiers .stl ou .3mf.",
"ERROR_ZERO_PRICE": "Un problème est survenu pendant le calcul. Essayez un autre format ou contactez-nous directement via Demander une consultation.",
"ZERO_RESULT_TITLE": "Résultat invalide",
"ZERO_RESULT_HELP": "Le calcul a renvoyé des valeurs nulles invalides. Essayez un autre format de fichier ou contactez-nous directement via Demander une consultation."
@@ -680,7 +681,7 @@
},
"DROPZONE": {
"DEFAULT_LABEL": "Glissez les fichiers ici ou cliquez pour téléverser",
"DEFAULT_SUBTEXT": "Prend en charge .STL, .3MF, .STEP"
"DEFAULT_SUBTEXT": "Prend en charge .STL, .3MF"
},
"COLOR": {
"AVAILABLE_COLORS": "Couleurs disponibles",

View File

@@ -84,11 +84,11 @@
"HERO_TITLE": "Servizio di stampa 3D.<br>Dal file al pezzo finito.",
"HERO_LEAD": "Con il calcolatore più avanzato per le tue stampe 3D: precisione assoluta e zero sorprese.",
"HERO_SUBTITLE": "Offriamo anche servizi di CAD per pezzi personalizzati!",
"HERO_SWISS_TITLE": "Based in Switzerland",
"HERO_SWISS_TITLE": "Con sede in Svizzera",
"HERO_SWISS_COPY": "Produzione e supporto in Svizzera",
"HERO_SWISS_LOCATIONS_LABEL": "Sedi",
"HERO_SWISS_LOCATION_1": "Ticino",
"HERO_SWISS_LOCATION_2": "Zurich",
"HERO_SWISS_LOCATION_2": "Zurigo",
"HERO_SWISS_LOCATION_3": "Biel/Bienne",
"HERO_SWISS_NOTE": "Operativi in tutta la Svizzera.",
"BTN_CALCULATE": "Calcola Preventivo",
@@ -96,7 +96,7 @@
"BTN_CONTACT": "Parla con noi",
"SEC_CALC_TITLE": "Prezzo corretto in pochi secondi",
"SEC_CALC_SUBTITLE": "Nessuna registrazione necessaria. La stima è effettuata tramite vero slicing.",
"SEC_CALC_LIST_1": "Formati supportati: STL, 3MF, STEP",
"SEC_CALC_LIST_1": "Formati supportati: STL, 3MF",
"CARD_CALC_EYEBROW": "Calcolo automatico",
"CARD_CALC_TITLE": "Prezzo e tempi in un click",
"CARD_CALC_TAG": "Senza registrazione",
@@ -139,14 +139,14 @@
},
"CALC": {
"TITLE": "Calcola Preventivo 3D",
"SUBTITLE": "Carica il tuo file 3D (STL, 3MF, STEP), imposta la qualità, il colore e calcola immediatamente prezzo e tempi.",
"SUBTITLE": "Carica il tuo file 3D (STL, 3MF), imposta la qualità, il colore e calcola immediatamente prezzo e tempi.",
"CTA_START": "Inizia Ora",
"BUSINESS": "Aziende",
"PRIVATE": "Privati",
"MODE_EASY": "Base",
"MODE_ADVANCED": "Avanzata",
"UPLOAD_LABEL": "Trascina il tuo file 3D qui",
"UPLOAD_SUB": "Supportiamo STL, 3MF, STEP fino a 50MB",
"UPLOAD_SUB": "Supportiamo STL, 3MF fino a 50MB",
"MATERIAL": "Materiale",
"QUALITY": "Qualità",
"PRINT_SPEED": "Velocità di Stampa",
@@ -185,11 +185,12 @@
"NOTES_PLACEHOLDER": "Istruzioni specifiche...",
"SETUP_NOTE": "* Include {{cost}} come costo di setup",
"SHIPPING_NOTE": "* Costi di spedizione esclusi, calcolati al passaggio successivo",
"STEP_WARNING": "La visualizzazione 3D non è compatibile con i file step e 3mf",
"STEP_WARNING": "La visualizzazione 3D è disponibile solo per i file STL",
"REMOVE_FILE": "Rimuovi file",
"FALLBACK_MATERIAL": "PLA (fallback)",
"FALLBACK_QUALITY_STANDARD": "Standard",
"ERR_FILE_TOO_LARGE": "Alcuni file superano il limite di 200MB e non sono stati aggiunti.",
"ERR_INVALID_FILE_TYPE": "Puoi caricare solo file .stl o .3mf.",
"ERROR_ZERO_PRICE": "Qualcosa è andato storto nel calcolo. Prova un altro formato o contattaci direttamente con Richiedi Consulenza.",
"ZERO_RESULT_TITLE": "Risultato non valido",
"ZERO_RESULT_HELP": "Il calcolo ha restituito valori non validi (0). Prova con un altro formato file oppure contattaci direttamente con Richiedi Consulenza."
@@ -680,7 +681,7 @@
},
"DROPZONE": {
"DEFAULT_LABEL": "Trascina i file qui o clicca per caricare",
"DEFAULT_SUBTEXT": "Supporta .STL, .3MF, .STEP"
"DEFAULT_SUBTEXT": "Supporta .STL, .3MF"
},
"COLOR": {
"AVAILABLE_COLORS": "Colori disponibili",

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 382 382" xml:space="preserve">
<path style="fill:#0077B7;" d="M347.445,0H34.555C15.471,0,0,15.471,0,34.555v312.889C0,366.529,15.471,382,34.555,382h312.889
C366.529,382,382,366.529,382,347.444V34.555C382,15.471,366.529,0,347.445,0z M118.207,329.844c0,5.554-4.502,10.056-10.056,10.056
H65.345c-5.554,0-10.056-4.502-10.056-10.056V150.403c0-5.554,4.502-10.056,10.056-10.056h42.806
c5.554,0,10.056,4.502,10.056,10.056V329.844z M86.748,123.432c-22.459,0-40.666-18.207-40.666-40.666S64.289,42.1,86.748,42.1
s40.666,18.207,40.666,40.666S109.208,123.432,86.748,123.432z M341.91,330.654c0,5.106-4.14,9.246-9.246,9.246H286.73
c-5.106,0-9.246-4.14-9.246-9.246v-84.168c0-12.556,3.683-55.021-32.813-55.021c-28.309,0-34.051,29.066-35.204,42.11v97.079
c0,5.106-4.139,9.246-9.246,9.246h-44.426c-5.106,0-9.246-4.14-9.246-9.246V149.593c0-5.106,4.14-9.246,9.246-9.246h44.426
c5.106,0,9.246,4.14,9.246,9.246v15.655c10.497-15.753,26.097-27.912,59.312-27.912c73.552,0,73.131,68.716,73.131,106.472
L341.91,330.654L341.91,330.654z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.16 163.48">
<g id="Livello_1-2" data-name="Livello 1">
<g>
<rect x=".25" y="157.08" width="125.66" height="1.05"/>
<rect x=".25" y="146.6" width="125.66" height="1.05"/>
<rect x=".25" y="136.13" width="125.66" height="1.05"/>
<rect x=".25" y="125.66" width="125.66" height="1.05"/>
<rect x=".25" y="115.19" width="125.66" height="1.05"/>
<rect x=".25" y="104.72" width="125.66" height="1.05"/>
<rect x=".25" y="94.25" width="125.66" height="1.05"/>
<rect x=".25" y="83.77" width="125.66" height="1.05"/>
<rect x=".25" y="73.3" width="125.66" height="1.05"/>
<rect x=".25" y="62.83" width="125.66" height="1.05"/>
<rect x=".25" y="52.36" width="125.66" height="1.05"/>
<rect x=".25" y="41.89" width="125.66" height="1.05"/>
<rect x=".25" y="31.42" width="125.66" height="1.05"/>
<rect x=".25" y="20.94" width="125.66" height="1.05"/>
<rect x=".25" y="10.47" width="125.66" height="1.05"/>
<rect x=".25" width="125.66" height="1.05"/>
</g>
<g>
<rect x="110.82" y="131.82" width="5.02" height="5.25"/>
<rect x="110.82" y="121.37" width="5.02" height="5.25"/>
<rect x="110.82" y="110.91" width="5.02" height="5.25"/>
<rect x="110.82" y="100.46" width="5.02" height="5.21"/>
<rect x="110.82" y="89.97" width="5.02" height="5.25"/>
<rect x="110.82" y="79.51" width="5.02" height="5.25"/>
<rect x="110.82" y="69.06" width="5.02" height="5.25"/>
<rect x="110.82" y="58.6" width="5.02" height="5.21"/>
<rect x="110.82" y="48.15" width="5.02" height="5.21"/>
<rect x="110.82" y="37.66" width="5.02" height="5.25"/>
<rect x="110.82" y="27.2" width="5.02" height="5.25"/>
<rect x="110.82" y="16.75" width="5.02" height="5.21"/>
<rect x="105.79" y="142.31" width="5.02" height="5.21"/>
<rect x="105.79" y="131.82" width="5.02" height="5.25"/>
<rect x="105.79" y="121.37" width="5.02" height="5.25"/>
<rect x="105.79" y="110.91" width="5.02" height="5.25"/>
<rect x="105.79" y="100.46" width="5.02" height="5.21"/>
<rect x="105.79" y="89.97" width="5.02" height="5.25"/>
<rect x="105.79" y="79.51" width="5.02" height="5.25"/>
<rect x="105.79" y="69.06" width="5.02" height="5.25"/>
<rect x="105.79" y="58.6" width="5.02" height="5.21"/>
<rect x="105.79" y="48.15" width="5.02" height="5.21"/>
<rect x="105.79" y="37.66" width="5.02" height="5.25"/>
<rect x="105.79" y="27.2" width="5.02" height="5.25"/>
<rect x="105.79" y="16.75" width="5.02" height="5.21"/>
<rect x="105.79" y="6.29" width="5.02" height="5.21"/>
<rect x="100.77" y="142.31" width="5.02" height="5.21"/>
<rect x="100.77" y="131.82" width="5.02" height="5.25"/>
<rect x="100.77" y="121.37" width="5.02" height="5.25"/>
<rect x="100.77" y="110.91" width="5.02" height="5.25"/>
<rect x="100.77" y="100.46" width="5.02" height="5.21"/>
<rect x="100.77" y="89.97" width="5.02" height="5.25"/>
<rect x="100.77" y="79.51" width="5.02" height="5.25"/>
<rect x="100.77" y="69.06" width="5.02" height="5.25"/>
<rect x="100.77" y="58.6" width="5.02" height="5.21"/>
<rect x="100.77" y="48.15" width="5.02" height="5.21"/>
<rect x="100.77" y="37.66" width="5.02" height="5.25"/>
<rect x="100.77" y="27.2" width="5.02" height="5.25"/>
<rect x="100.77" y="16.75" width="5.02" height="5.21"/>
<rect x="100.77" y="6.29" width="5.02" height="5.21"/>
<rect x="95.75" y="142.31" width="5.02" height="5.21"/>
<rect x="95.75" y="131.82" width="5.02" height="5.25"/>
<rect x="95.75" y="121.37" width="5.02" height="5.25"/>
<rect x="95.75" y="110.91" width="5.02" height="5.25"/>
<rect x="95.75" y="100.46" width="5.02" height="5.21"/>
<rect x="95.75" y="89.97" width="5.02" height="5.25"/>
<rect x="95.75" y="79.51" width="5.02" height="5.25"/>
<rect x="95.75" y="69.06" width="5.02" height="5.25"/>
<rect x="95.75" y="58.6" width="5.02" height="5.21"/>
<rect x="95.75" y="48.15" width="5.02" height="5.21"/>
<rect x="95.75" y="37.66" width="5.02" height="5.25"/>
<rect x="95.75" y="27.2" width="5.02" height="5.25"/>
<rect x="95.75" y="16.75" width="5.02" height="5.21"/>
<rect x="95.75" y="6.29" width="5.02" height="5.21"/>
<rect x="90.72" y="142.31" width="5.02" height="5.21"/>
<rect x="90.72" y="131.82" width="5.02" height="5.25"/>
<rect x="90.72" y="121.37" width="5.02" height="5.25"/>
<rect x="90.72" y="110.91" width="5.02" height="5.25"/>
<rect x="90.72" y="100.46" width="5.02" height="5.21"/>
<rect x="90.72" y="89.97" width="5.02" height="5.25"/>
<rect x="90.72" y="79.51" width="5.02" height="5.25"/>
<rect x="90.72" y="69.06" width="5.02" height="5.25"/>
<rect x="90.72" y="58.6" width="5.02" height="5.21"/>
<rect x="90.72" y="48.15" width="5.02" height="5.21"/>
<rect x="90.72" y="37.66" width="5.02" height="5.25"/>
<rect x="90.72" y="27.2" width="5.02" height="5.25"/>
<rect x="90.72" y="16.75" width="5.02" height="5.21"/>
<rect x="90.72" y="6.29" width="5.02" height="5.21"/>
<rect x="85.7" y="142.31" width="5.02" height="5.21"/>
<rect x="85.7" y="131.82" width="5.02" height="5.25"/>
<rect x="85.7" y="121.37" width="5.02" height="5.25"/>
<rect x="85.7" y="110.91" width="5.02" height="5.25"/>
<rect x="85.7" y="100.46" width="5.02" height="5.21"/>
<rect x="85.7" y="89.97" width="5.02" height="5.25"/>
<rect x="85.7" y="79.51" width="5.02" height="5.25"/>
<rect x="85.7" y="69.06" width="5.02" height="5.25"/>
<rect x="85.7" y="58.6" width="5.02" height="5.21"/>
<rect x="85.7" y="48.15" width="5.02" height="5.21"/>
<rect x="85.7" y="37.66" width="5.02" height="5.25"/>
<rect x="85.7" y="27.2" width="5.02" height="5.25"/>
<rect x="85.7" y="16.75" width="5.02" height="5.21"/>
<rect x="85.7" y="6.29" width="5.02" height="5.21"/>
<rect x="80.67" y="142.31" width="5.02" height="5.21"/>
<rect x="80.67" y="131.82" width="5.02" height="5.25"/>
<rect x="80.67" y="121.37" width="5.02" height="5.25"/>
<rect x="80.67" y="79.51" width="5.02" height="5.25"/>
<rect x="80.67" y="69.06" width="5.02" height="5.25"/>
<rect x="80.67" y="27.2" width="5.02" height="5.25"/>
<rect x="80.67" y="16.75" width="5.02" height="5.21"/>
<rect x="80.67" y="6.29" width="5.02" height="5.21"/>
<rect x="75.65" y="142.31" width="5.02" height="5.21"/>
<rect x="75.65" y="131.82" width="5.02" height="5.25"/>
<rect x="75.65" y="121.37" width="5.02" height="5.25"/>
<rect x="75.65" y="79.51" width="5.02" height="5.25"/>
<rect x="75.65" y="69.06" width="5.02" height="5.25"/>
<rect x="75.65" y="27.2" width="5.02" height="5.25"/>
<rect x="75.65" y="16.75" width="5.02" height="5.21"/>
<rect x="75.65" y="6.29" width="5.02" height="5.21"/>
<rect x="70.63" y="142.31" width="5.02" height="5.21"/>
<rect x="70.63" y="131.82" width="5.02" height="5.25"/>
<rect x="70.63" y="121.37" width="5.02" height="5.25"/>
<rect x="70.63" y="79.51" width="5.02" height="5.25"/>
<rect x="70.63" y="69.06" width="5.02" height="5.25"/>
<rect x="70.63" y="27.2" width="5.02" height="5.25"/>
<rect x="70.63" y="16.75" width="5.02" height="5.21"/>
<rect x="70.63" y="6.29" width="5.02" height="5.21"/>
<rect x="65.6" y="142.31" width="5.02" height="5.21"/>
<rect x="65.6" y="131.82" width="5.02" height="5.25"/>
<rect x="65.6" y="121.37" width="5.02" height="5.25"/>
<rect x="65.6" y="79.51" width="5.02" height="5.25"/>
<rect x="65.6" y="69.06" width="5.02" height="5.25"/>
<rect x="65.6" y="27.2" width="5.02" height="5.25"/>
<rect x="65.6" y="16.75" width="5.02" height="5.21"/>
<rect x="65.6" y="6.29" width="5.02" height="5.21"/>
<rect x="60.58" y="142.31" width="5.02" height="5.21"/>
<rect x="60.58" y="131.82" width="5.02" height="5.25"/>
<rect x="60.58" y="121.37" width="5.02" height="5.25"/>
<rect x="60.58" y="79.51" width="5.02" height="5.25"/>
<rect x="60.58" y="69.06" width="5.02" height="5.25"/>
<rect x="60.58" y="27.2" width="5.02" height="5.25"/>
<rect x="60.58" y="16.75" width="5.02" height="5.21"/>
<rect x="60.58" y="6.29" width="5.02" height="5.21"/>
<rect x="55.55" y="142.31" width="5.02" height="5.21"/>
<rect x="55.55" y="131.82" width="5.02" height="5.25"/>
<rect x="55.55" y="121.37" width="5.02" height="5.25"/>
<rect x="55.55" y="79.51" width="5.02" height="5.25"/>
<rect x="55.55" y="69.06" width="5.02" height="5.25"/>
<rect x="55.55" y="27.2" width="5.02" height="5.25"/>
<rect x="55.55" y="16.75" width="5.02" height="5.21"/>
<rect x="55.55" y="6.29" width="5.02" height="5.21"/>
<rect x="50.53" y="142.31" width="5.02" height="5.21"/>
<rect x="50.53" y="131.82" width="5.02" height="5.25"/>
<rect x="50.53" y="121.37" width="5.02" height="5.25"/>
<rect x="50.53" y="79.51" width="5.02" height="5.25"/>
<rect x="50.53" y="69.06" width="5.02" height="5.25"/>
<rect x="50.53" y="27.2" width="5.02" height="5.25"/>
<rect x="50.53" y="16.75" width="5.02" height="5.21"/>
<rect x="50.53" y="6.29" width="5.02" height="5.21"/>
<rect x="45.51" y="142.31" width="5.02" height="5.21"/>
<rect x="45.51" y="131.82" width="5.02" height="5.25"/>
<rect x="45.51" y="121.37" width="5.02" height="5.25"/>
<rect x="45.51" y="27.2" width="5.02" height="5.25"/>
<rect x="45.51" y="16.75" width="5.02" height="5.21"/>
<rect x="45.51" y="6.29" width="5.02" height="5.21"/>
<rect x="40.44" y="142.31" width="5.06" height="5.21"/>
<rect x="40.44" y="131.82" width="5.06" height="5.25"/>
<rect x="40.44" y="121.37" width="5.06" height="5.25"/>
<rect x="40.44" y="27.2" width="5.06" height="5.25"/>
<rect x="40.44" y="16.75" width="5.06" height="5.21"/>
<rect x="40.44" y="6.29" width="5.06" height="5.21"/>
<rect x="35.42" y="142.31" width="5.02" height="5.21"/>
<rect x="35.42" y="131.82" width="5.02" height="5.25"/>
<rect x="35.42" y="121.37" width="5.02" height="5.25"/>
<rect x="35.42" y="27.2" width="5.02" height="5.25"/>
<rect x="35.42" y="16.75" width="5.02" height="5.21"/>
<rect x="35.42" y="6.29" width="5.02" height="5.21"/>
<rect x="30.4" y="142.31" width="5.02" height="5.21"/>
<rect x="30.4" y="131.82" width="5.02" height="5.25"/>
<rect x="30.4" y="121.37" width="5.02" height="5.25"/>
<rect x="30.4" y="27.2" width="5.02" height="5.25"/>
<rect x="30.4" y="16.75" width="5.02" height="5.21"/>
<rect x="30.4" y="6.29" width="5.02" height="5.21"/>
<rect x="25.37" y="142.31" width="5.02" height="5.21"/>
<rect x="25.37" y="131.82" width="5.02" height="5.25"/>
<rect x="25.37" y="121.37" width="5.02" height="5.25"/>
<rect x="25.37" y="27.2" width="5.02" height="5.25"/>
<rect x="25.37" y="16.75" width="5.02" height="5.21"/>
<rect x="25.37" y="6.29" width="5.02" height="5.21"/>
<rect x="20.35" y="142.31" width="5.02" height="5.21"/>
<rect x="20.35" y="131.82" width="5.02" height="5.25"/>
<rect x="20.35" y="121.37" width="5.02" height="5.25"/>
<rect x="20.35" y="27.2" width="5.02" height="5.25"/>
<rect x="20.35" y="16.75" width="5.02" height="5.21"/>
<rect x="20.35" y="6.29" width="5.02" height="5.21"/>
<rect x="15.32" y="142.31" width="5.02" height="5.21"/>
<rect x="15.32" y="131.82" width="5.02" height="5.25"/>
<rect x="15.32" y="121.37" width="5.02" height="5.25"/>
<rect x="15.32" y="27.2" width="5.02" height="5.25"/>
<rect x="15.32" y="16.75" width="5.02" height="5.21"/>
<rect x="15.32" y="6.29" width="5.02" height="5.21"/>
<rect x="10.3" y="131.82" width="5.02" height="5.25"/>
<rect x="10.3" y="16.75" width="5.02" height="5.21"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,209 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.16 163.48">
<defs>
</defs>
<g id="Livello_1-2" data-name="Livello 1">
<g>
<rect class="cls-2" x=".25" y="157.08" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="146.6" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="136.13" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="125.66" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="115.19" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="104.72" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="94.25" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="83.77" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="73.3" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="62.83" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="52.36" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="41.89" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="31.42" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="20.94" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="10.47" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" width="125.66" height="1.05"/>
</g>
<g>
<rect class="cls-1" x="110.83" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.83" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.83" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.83" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="110.83" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="110.83" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.83" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.83" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="110.83" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="110.83" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.83" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.83" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.81" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="105.81" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.81" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.81" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.81" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="105.81" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="105.81" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.81" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.81" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="105.81" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="105.81" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.81" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.81" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.81" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.78" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.78" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.78" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.78" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.78" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.78" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.78" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.78" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.78" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.78" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.78" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.78" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.78" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.78" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.76" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.76" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.76" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.76" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.76" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.76" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.76" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.76" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.76" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.76" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.76" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.76" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.76" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.76" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.73" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.73" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.73" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.73" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.73" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.73" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.73" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.73" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.73" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.73" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.73" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.73" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.73" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.73" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="85.71" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="85.71" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="85.71" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="85.71" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="85.71" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="85.71" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="85.71" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="85.71" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="85.71" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="85.71" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="85.71" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="85.71" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="85.71" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="85.71" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="80.69" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="80.69" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.69" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.69" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.69" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.69" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.69" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.69" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="75.66" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="75.66" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.66" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.66" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.66" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.66" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.66" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.66" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="70.64" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="70.64" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.64" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.64" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.64" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.64" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.64" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.64" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="65.58" y="142.31" width="5.06" height="5.21"/>
<rect class="cls-1" x="65.58" y="131.82" width="5.06" height="5.25"/>
<rect class="cls-1" x="65.58" y="121.37" width="5.06" height="5.25"/>
<rect class="cls-1" x="65.58" y="79.51" width="5.06" height="5.25"/>
<rect class="cls-1" x="65.58" y="69.06" width="5.06" height="5.25"/>
<rect class="cls-1" x="65.58" y="27.2" width="5.06" height="5.25"/>
<rect class="cls-1" x="65.58" y="16.75" width="5.06" height="5.25"/>
<rect class="cls-1" x="65.58" y="6.29" width="5.06" height="5.21"/>
<rect class="cls-1" x="60.55" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="60.55" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.55" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.55" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.55" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.55" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.55" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.55" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="55.53" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="55.53" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.53" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.53" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.53" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.53" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.53" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.53" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="50.5" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="50.5" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.5" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.5" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.5" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.5" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.5" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.5" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="45.48" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="45.48" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="45.48" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="45.48" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="45.48" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="45.48" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="40.46" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="40.46" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.46" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.46" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.46" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.46" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.43" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.43" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.43" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.43" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.43" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.43" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.41" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.41" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.41" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.41" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.41" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.41" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.38" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.38" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.38" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.38" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.38" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.38" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.36" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.36" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.36" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.36" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.36" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.36" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.34" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.34" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.34" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.34" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.34" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.34" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.31" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.31" y="16.75" width="5.02" height="5.25"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,225 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.16 163.48">
<g id="Livello_1-2" data-name="Livello 1">
<g>
<rect x=".25" y="157.08" width="125.66" height="1.05"/>
<rect x=".25" y="146.6" width="125.66" height="1.05"/>
<rect x=".25" y="136.13" width="125.66" height="1.05"/>
<rect x=".25" y="125.66" width="125.66" height="1.05"/>
<rect x=".25" y="115.19" width="125.66" height="1.05"/>
<rect x=".25" y="104.72" width="125.66" height="1.05"/>
<rect x=".25" y="94.25" width="125.66" height="1.05"/>
<rect x=".25" y="83.77" width="125.66" height="1.05"/>
<rect x=".25" y="73.3" width="125.66" height="1.05"/>
<rect x=".25" y="62.83" width="125.66" height="1.05"/>
<rect x=".25" y="52.36" width="125.66" height="1.05"/>
<rect x=".25" y="41.89" width="125.66" height="1.05"/>
<rect x=".25" y="31.42" width="125.66" height="1.05"/>
<rect x=".25" y="20.94" width="125.66" height="1.05"/>
<rect x=".25" y="10.47" width="125.66" height="1.05"/>
<rect x=".25" width="125.66" height="1.05"/>
</g>
<g>
<rect x="115.73" y="142.22" width="3.4" height="5.28"/>
<rect x="112.33" y="142.22" width="3.4" height="5.28"/>
<rect x="112.33" y="131.72" width="3.4" height="5.25"/>
<rect x="108.93" y="142.22" width="3.4" height="5.28"/>
<rect x="108.93" y="131.72" width="3.4" height="5.25"/>
<rect x="108.93" y="121.23" width="3.4" height="5.25"/>
<rect x="105.53" y="142.22" width="3.4" height="5.28"/>
<rect x="105.53" y="131.72" width="3.4" height="5.25"/>
<rect x="105.53" y="121.23" width="3.4" height="5.25"/>
<rect x="105.53" y="110.74" width="3.4" height="5.25"/>
<rect x="102.14" y="142.22" width="3.4" height="5.28"/>
<rect x="102.14" y="131.72" width="3.4" height="5.25"/>
<rect x="102.14" y="121.23" width="3.4" height="5.25"/>
<rect x="102.14" y="110.74" width="3.4" height="5.25"/>
<rect x="102.14" y="100.25" width="3.4" height="5.25"/>
<rect x="98.74" y="142.22" width="3.4" height="5.28"/>
<rect x="98.74" y="131.72" width="3.4" height="5.25"/>
<rect x="98.74" y="121.23" width="3.4" height="5.25"/>
<rect x="98.74" y="110.74" width="3.4" height="5.25"/>
<rect x="98.74" y="100.25" width="3.4" height="5.25"/>
<rect x="98.74" y="89.76" width="3.4" height="5.25"/>
<rect x="95.34" y="142.22" width="3.4" height="5.28"/>
<rect x="95.34" y="131.72" width="3.4" height="5.25"/>
<rect x="95.34" y="121.23" width="3.4" height="5.25"/>
<rect x="95.34" y="110.74" width="3.4" height="5.25"/>
<rect x="95.34" y="100.25" width="3.4" height="5.25"/>
<rect x="95.34" y="89.76" width="3.4" height="5.25"/>
<rect x="95.34" y="79.27" width="3.4" height="5.25"/>
<rect x="91.94" y="131.72" width="3.4" height="5.25"/>
<rect x="91.94" y="121.23" width="3.4" height="5.25"/>
<rect x="91.94" y="110.74" width="3.4" height="5.25"/>
<rect x="91.94" y="100.25" width="3.4" height="5.25"/>
<rect x="91.94" y="89.76" width="3.4" height="5.25"/>
<rect x="91.94" y="79.27" width="3.4" height="5.25"/>
<rect x="91.94" y="68.74" width="3.4" height="5.28"/>
<rect x="88.54" y="121.23" width="3.4" height="5.25"/>
<rect x="88.54" y="110.74" width="3.4" height="5.25"/>
<rect x="88.54" y="100.25" width="3.4" height="5.25"/>
<rect x="88.54" y="89.76" width="3.4" height="5.25"/>
<rect x="88.54" y="79.27" width="3.4" height="5.25"/>
<rect x="88.54" y="68.74" width="3.4" height="5.28"/>
<rect x="88.54" y="58.25" width="3.4" height="5.25"/>
<rect x="85.14" y="110.74" width="3.4" height="5.25"/>
<rect x="85.14" y="100.25" width="3.4" height="5.25"/>
<rect x="85.14" y="89.76" width="3.4" height="5.25"/>
<rect x="85.14" y="79.27" width="3.4" height="5.25"/>
<rect x="85.14" y="68.74" width="3.4" height="5.28"/>
<rect x="85.14" y="58.25" width="3.4" height="5.25"/>
<rect x="85.14" y="47.75" width="3.4" height="5.25"/>
<rect x="81.74" y="100.25" width="3.4" height="5.25"/>
<rect x="81.74" y="89.76" width="3.4" height="5.25"/>
<rect x="81.74" y="79.27" width="3.4" height="5.25"/>
<rect x="81.74" y="68.74" width="3.4" height="5.28"/>
<rect x="81.74" y="58.25" width="3.4" height="5.25"/>
<rect x="81.74" y="47.75" width="3.4" height="5.25"/>
<rect x="81.74" y="37.26" width="3.4" height="5.25"/>
<rect x="78.38" y="100.25" width="3.36" height="5.25"/>
<rect x="78.38" y="89.76" width="3.36" height="5.25"/>
<rect x="78.38" y="79.27" width="3.36" height="5.25"/>
<rect x="78.38" y="68.74" width="3.36" height="5.28"/>
<rect x="78.38" y="58.25" width="3.36" height="5.25"/>
<rect x="78.38" y="47.75" width="3.36" height="5.25"/>
<rect x="78.38" y="37.26" width="3.36" height="5.25"/>
<rect x="78.38" y="26.77" width="3.36" height="5.25"/>
<rect x="74.98" y="100.25" width="3.4" height="5.25"/>
<rect x="74.98" y="89.76" width="3.4" height="5.25"/>
<rect x="74.98" y="79.27" width="3.4" height="5.25"/>
<rect x="74.98" y="68.74" width="3.4" height="5.28"/>
<rect x="74.98" y="58.25" width="3.4" height="5.25"/>
<rect x="74.98" y="47.75" width="3.4" height="5.25"/>
<rect x="74.98" y="37.26" width="3.4" height="5.25"/>
<rect x="74.98" y="26.77" width="3.4" height="5.25"/>
<rect x="74.98" y="16.28" width="3.4" height="5.25"/>
<rect x="71.58" y="100.25" width="3.4" height="5.25"/>
<rect x="71.58" y="89.76" width="3.4" height="5.25"/>
<rect x="71.58" y="68.74" width="3.4" height="5.28"/>
<rect x="71.58" y="58.25" width="3.4" height="5.25"/>
<rect x="71.58" y="47.75" width="3.4" height="5.25"/>
<rect x="71.58" y="37.26" width="3.4" height="5.25"/>
<rect x="71.58" y="26.77" width="3.4" height="5.25"/>
<rect x="71.58" y="16.28" width="3.4" height="5.25"/>
<rect x="71.58" y="5.79" width="3.4" height="5.25"/>
<rect x="68.19" y="100.25" width="3.4" height="5.25"/>
<rect x="68.19" y="89.76" width="3.4" height="5.25"/>
<rect x="68.19" y="58.25" width="3.4" height="5.25"/>
<rect x="68.19" y="47.75" width="3.4" height="5.25"/>
<rect x="68.19" y="37.26" width="3.4" height="5.25"/>
<rect x="68.19" y="26.77" width="3.4" height="5.25"/>
<rect x="68.19" y="16.28" width="3.4" height="5.25"/>
<rect x="68.19" y="5.79" width="3.4" height="5.25"/>
<rect x="64.79" y="100.25" width="3.4" height="5.25"/>
<rect x="64.79" y="89.76" width="3.4" height="5.25"/>
<rect x="64.79" y="47.75" width="3.4" height="5.25"/>
<rect x="64.79" y="37.26" width="3.4" height="5.25"/>
<rect x="64.79" y="26.77" width="3.4" height="5.25"/>
<rect x="64.79" y="16.28" width="3.4" height="5.25"/>
<rect x="64.79" y="5.79" width="3.4" height="5.25"/>
<rect x="61.39" y="100.25" width="3.4" height="5.25"/>
<rect x="61.39" y="89.76" width="3.4" height="5.25"/>
<rect x="61.39" y="37.26" width="3.4" height="5.25"/>
<rect x="61.39" y="26.77" width="3.4" height="5.25"/>
<rect x="61.39" y="16.28" width="3.4" height="5.25"/>
<rect x="61.39" y="5.79" width="3.4" height="5.25"/>
<rect x="57.99" y="100.25" width="3.4" height="5.25"/>
<rect x="57.99" y="89.76" width="3.4" height="5.25"/>
<rect x="57.99" y="47.75" width="3.4" height="5.25"/>
<rect x="57.99" y="37.26" width="3.4" height="5.25"/>
<rect x="57.99" y="26.77" width="3.4" height="5.25"/>
<rect x="57.99" y="16.28" width="3.4" height="5.25"/>
<rect x="57.99" y="5.79" width="3.4" height="5.25"/>
<rect x="54.59" y="100.25" width="3.4" height="5.25"/>
<rect x="54.59" y="89.76" width="3.4" height="5.25"/>
<rect x="54.59" y="58.25" width="3.4" height="5.25"/>
<rect x="54.59" y="47.75" width="3.4" height="5.25"/>
<rect x="54.59" y="37.26" width="3.4" height="5.25"/>
<rect x="54.59" y="26.77" width="3.4" height="5.25"/>
<rect x="54.59" y="16.28" width="3.4" height="5.25"/>
<rect x="54.59" y="5.79" width="3.4" height="5.25"/>
<rect x="51.19" y="100.25" width="3.4" height="5.25"/>
<rect x="51.19" y="89.76" width="3.4" height="5.25"/>
<rect x="51.19" y="68.74" width="3.4" height="5.28"/>
<rect x="51.19" y="58.25" width="3.4" height="5.25"/>
<rect x="51.19" y="47.75" width="3.4" height="5.25"/>
<rect x="51.19" y="37.26" width="3.4" height="5.25"/>
<rect x="51.19" y="26.77" width="3.4" height="5.25"/>
<rect x="51.19" y="16.28" width="3.4" height="5.25"/>
<rect x="51.19" y="5.79" width="3.4" height="5.25"/>
<rect x="47.79" y="100.25" width="3.4" height="5.25"/>
<rect x="47.79" y="89.76" width="3.4" height="5.25"/>
<rect x="47.79" y="79.27" width="3.4" height="5.25"/>
<rect x="47.79" y="68.74" width="3.4" height="5.28"/>
<rect x="47.79" y="58.25" width="3.4" height="5.25"/>
<rect x="47.79" y="47.75" width="3.4" height="5.25"/>
<rect x="47.79" y="37.26" width="3.4" height="5.25"/>
<rect x="47.79" y="26.77" width="3.4" height="5.25"/>
<rect x="47.79" y="16.28" width="3.4" height="5.25"/>
<rect x="44.39" y="100.25" width="3.4" height="5.25"/>
<rect x="44.39" y="89.76" width="3.4" height="5.25"/>
<rect x="44.39" y="79.27" width="3.4" height="5.25"/>
<rect x="44.39" y="68.74" width="3.4" height="5.28"/>
<rect x="44.39" y="58.25" width="3.4" height="5.25"/>
<rect x="44.39" y="47.75" width="3.4" height="5.25"/>
<rect x="44.39" y="37.26" width="3.4" height="5.25"/>
<rect x="44.39" y="26.77" width="3.4" height="5.25"/>
<rect x="41" y="100.25" width="3.4" height="5.25"/>
<rect x="41" y="89.76" width="3.4" height="5.25"/>
<rect x="41" y="79.27" width="3.4" height="5.25"/>
<rect x="41" y="68.74" width="3.4" height="5.28"/>
<rect x="41" y="58.25" width="3.4" height="5.25"/>
<rect x="41" y="47.75" width="3.4" height="5.25"/>
<rect x="41" y="37.26" width="3.4" height="5.25"/>
<rect x="37.6" y="110.74" width="3.4" height="5.25"/>
<rect x="37.6" y="100.25" width="3.4" height="5.25"/>
<rect x="37.6" y="89.76" width="3.4" height="5.25"/>
<rect x="37.6" y="79.27" width="3.4" height="5.25"/>
<rect x="37.6" y="68.74" width="3.4" height="5.28"/>
<rect x="37.6" y="58.25" width="3.4" height="5.25"/>
<rect x="37.6" y="47.75" width="3.4" height="5.25"/>
<rect x="34.2" y="121.23" width="3.4" height="5.25"/>
<rect x="34.2" y="110.74" width="3.4" height="5.25"/>
<rect x="34.2" y="100.25" width="3.4" height="5.25"/>
<rect x="34.2" y="89.76" width="3.4" height="5.25"/>
<rect x="34.2" y="79.27" width="3.4" height="5.25"/>
<rect x="34.2" y="68.74" width="3.4" height="5.28"/>
<rect x="34.2" y="58.25" width="3.4" height="5.25"/>
<rect x="30.8" y="131.72" width="3.4" height="5.25"/>
<rect x="30.8" y="121.23" width="3.4" height="5.25"/>
<rect x="30.8" y="110.74" width="3.4" height="5.25"/>
<rect x="30.8" y="100.25" width="3.4" height="5.25"/>
<rect x="30.8" y="89.76" width="3.4" height="5.25"/>
<rect x="30.8" y="79.27" width="3.4" height="5.25"/>
<rect x="30.8" y="68.74" width="3.4" height="5.28"/>
<rect x="27.4" y="142.22" width="3.4" height="5.28"/>
<rect x="27.4" y="131.72" width="3.4" height="5.25"/>
<rect x="27.4" y="121.23" width="3.4" height="5.25"/>
<rect x="27.4" y="110.74" width="3.4" height="5.25"/>
<rect x="27.4" y="100.25" width="3.4" height="5.25"/>
<rect x="27.4" y="89.76" width="3.4" height="5.25"/>
<rect x="27.4" y="79.27" width="3.4" height="5.25"/>
<rect x="24.04" y="142.22" width="3.36" height="5.28"/>
<rect x="24.04" y="131.72" width="3.36" height="5.25"/>
<rect x="24.04" y="121.23" width="3.36" height="5.25"/>
<rect x="24.04" y="110.74" width="3.36" height="5.25"/>
<rect x="24.04" y="100.25" width="3.36" height="5.25"/>
<rect x="24.04" y="89.76" width="3.36" height="5.25"/>
<rect x="20.64" y="142.22" width="3.4" height="5.28"/>
<rect x="20.64" y="131.72" width="3.4" height="5.25"/>
<rect x="20.64" y="121.23" width="3.4" height="5.25"/>
<rect x="20.64" y="110.74" width="3.4" height="5.25"/>
<rect x="20.64" y="100.25" width="3.4" height="5.25"/>
<rect x="17.24" y="142.22" width="3.4" height="5.28"/>
<rect x="17.24" y="131.72" width="3.4" height="5.25"/>
<rect x="17.24" y="121.23" width="3.4" height="5.25"/>
<rect x="17.24" y="110.74" width="3.4" height="5.25"/>
<rect x="13.84" y="142.22" width="3.4" height="5.28"/>
<rect x="13.84" y="131.72" width="3.4" height="5.25"/>
<rect x="13.84" y="121.23" width="3.4" height="5.25"/>
<rect x="10.45" y="142.22" width="3.4" height="5.28"/>
<rect x="10.45" y="131.72" width="3.4" height="5.25"/>
<rect x="7.05" y="142.22" width="3.4" height="5.28"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,236 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.16 164">
<defs>
<style>
.cls-1 {
fill: #ffdf07;
}
.cls-2 {
fill: #575756;
}
</style>
</defs>
<g id="Livello_1-2" data-name="Livello 1">
<g>
<rect class="cls-2" x=".25" y="157.08" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="146.6" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="136.13" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="125.66" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="115.19" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="104.72" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="94.25" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="83.77" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="73.3" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="62.83" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="52.36" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="41.89" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="31.42" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="20.94" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="10.47" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" width="125.66" height="1.05"/>
</g>
<g>
<rect class="cls-1" x="115.71" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="112.34" y="142.77" width="3.36" height="5.25"/>
<rect class="cls-1" x="112.34" y="132.24" width="3.36" height="5.25"/>
<rect class="cls-1" x="108.94" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="108.94" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="108.94" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="105.55" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="105.55" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="105.55" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="105.55" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="102.15" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="102.15" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="102.15" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="102.15" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="102.15" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="98.75" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="98.75" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="98.75" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="98.75" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="98.75" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="98.75" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="95.35" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="95.35" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="95.35" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="95.35" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="95.35" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="95.35" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="95.35" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="91.95" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="91.95" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="91.95" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="91.95" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="91.95" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="91.95" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="91.95" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="88.55" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="88.55" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="88.55" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="88.55" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="88.55" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="88.55" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="88.55" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="85.15" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="85.15" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="85.15" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="85.15" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="85.15" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="85.15" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="85.15" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="81.76" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="81.76" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="81.76" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="81.76" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="81.76" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="81.76" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="81.76" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="78.36" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="78.36" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="78.36" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="78.36" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="78.36" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="78.36" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="78.36" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="78.36" y="27.29" width="3.4" height="5.25"/>
<rect class="cls-1" x="74.96" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="74.96" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="74.96" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="74.96" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="74.96" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="74.96" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="74.96" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="74.96" y="27.29" width="3.4" height="5.25"/>
<rect class="cls-1" x="74.96" y="16.8" width="3.4" height="5.25"/>
<rect class="cls-1" x="71.56" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="71.56" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="71.56" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="71.56" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="71.56" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="71.56" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="71.56" y="27.29" width="3.4" height="5.25"/>
<rect class="cls-1" x="71.56" y="16.8" width="3.4" height="5.25"/>
<rect class="cls-1" x="71.56" y="6.31" width="3.4" height="5.25"/>
<rect class="cls-1" x="68.16" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="68.16" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="68.16" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="68.16" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="68.16" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="68.16" y="27.29" width="3.4" height="5.25"/>
<rect class="cls-1" x="68.16" y="16.8" width="3.4" height="5.25"/>
<rect class="cls-1" x="68.16" y="6.31" width="3.4" height="5.25"/>
<rect class="cls-1" x="64.76" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="64.76" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="64.76" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="64.76" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="64.76" y="27.29" width="3.4" height="5.25"/>
<rect class="cls-1" x="64.76" y="16.8" width="3.4" height="5.25"/>
<rect class="cls-1" x="64.76" y="6.31" width="3.4" height="5.25"/>
<rect class="cls-1" x="61.4" y="100.77" width="3.36" height="5.25"/>
<rect class="cls-1" x="61.4" y="90.28" width="3.36" height="5.25"/>
<rect class="cls-1" x="61.4" y="37.78" width="3.36" height="5.25"/>
<rect class="cls-1" x="61.4" y="27.29" width="3.36" height="5.25"/>
<rect class="cls-1" x="61.4" y="16.8" width="3.36" height="5.25"/>
<rect class="cls-1" x="61.4" y="6.31" width="3.36" height="5.25"/>
<rect class="cls-1" x="58" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="58" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="58" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="58" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="58" y="27.29" width="3.4" height="5.25"/>
<rect class="cls-1" x="58" y="16.8" width="3.4" height="5.25"/>
<rect class="cls-1" x="58" y="6.31" width="3.4" height="5.25"/>
<rect class="cls-1" x="54.6" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="54.6" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="54.6" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="54.6" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="54.6" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="54.6" y="27.29" width="3.4" height="5.25"/>
<rect class="cls-1" x="54.6" y="16.8" width="3.4" height="5.25"/>
<rect class="cls-1" x="54.6" y="6.31" width="3.4" height="5.25"/>
<rect class="cls-1" x="51.2" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="51.2" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="51.2" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="51.2" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="51.2" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="51.2" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="51.2" y="27.29" width="3.4" height="5.25"/>
<rect class="cls-1" x="51.2" y="16.8" width="3.4" height="5.25"/>
<rect class="cls-1" x="51.2" y="6.31" width="3.4" height="5.25"/>
<rect class="cls-1" x="47.81" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="47.81" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="47.81" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="47.81" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="47.81" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="47.81" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="47.81" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="47.81" y="27.29" width="3.4" height="5.25"/>
<rect class="cls-1" x="47.81" y="16.8" width="3.4" height="5.25"/>
<rect class="cls-1" x="44.41" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="44.41" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="44.41" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="44.41" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="44.41" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="44.41" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="44.41" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="44.41" y="27.29" width="3.4" height="5.25"/>
<rect class="cls-1" x="41.01" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="41.01" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="41.01" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="41.01" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="41.01" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="41.01" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="41.01" y="37.78" width="3.4" height="5.25"/>
<rect class="cls-1" x="37.61" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="37.61" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="37.61" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="37.61" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="37.61" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="37.61" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="37.61" y="48.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="34.21" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="34.21" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="34.21" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="34.21" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="34.21" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="34.21" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="34.21" y="58.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="30.81" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="30.81" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="30.81" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="30.81" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="30.81" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="30.81" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="30.81" y="69.3" width="3.4" height="5.25"/>
<rect class="cls-1" x="27.41" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="27.41" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="27.41" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="27.41" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="27.41" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="27.41" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="27.41" y="79.79" width="3.4" height="5.25"/>
<rect class="cls-1" x="24.01" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="24.01" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="24.01" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="24.01" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="24.01" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="24.01" y="90.28" width="3.4" height="5.25"/>
<rect class="cls-1" x="20.62" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="20.62" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="20.62" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="20.62" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="20.62" y="100.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="17.22" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="17.22" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="17.22" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="17.22" y="111.26" width="3.4" height="5.25"/>
<rect class="cls-1" x="13.82" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="13.82" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="13.82" y="121.75" width="3.4" height="5.25"/>
<rect class="cls-1" x="10.42" y="142.77" width="3.4" height="5.25"/>
<rect class="cls-1" x="10.42" y="132.24" width="3.4" height="5.25"/>
<rect class="cls-1" x="7.06" y="142.77" width="3.36" height="5.25"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,232 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129.61 163.48">
<g id="Livello_1-2" data-name="Livello 1">
<g>
<rect y="157.08" width="125.66" height="1.05"/>
<rect y="146.6" width="125.66" height="1.05"/>
<rect y="136.13" width="125.66" height="1.05"/>
<rect y="125.66" width="125.66" height="1.05"/>
<rect y="115.19" width="125.66" height="1.05"/>
<rect y="104.72" width="125.66" height="1.05"/>
<rect y="94.25" width="125.66" height="1.05"/>
<rect y="83.77" width="125.66" height="1.05"/>
<rect y="73.3" width="125.66" height="1.05"/>
<rect y="62.83" width="125.66" height="1.05"/>
<rect y="52.36" width="125.66" height="1.05"/>
<rect y="41.89" width="125.66" height="1.05"/>
<rect y="31.42" width="125.66" height="1.05"/>
<rect y="20.94" width="125.66" height="1.05"/>
<rect y="10.47" width="125.66" height="1.05"/>
<rect width="125.66" height="1.05"/>
</g>
<g>
<rect x="114.29" y="121.36" width="5.02" height="5.25"/>
<rect x="114.29" y="110.9" width="5.02" height="5.25"/>
<rect x="114.29" y="100.45" width="5.02" height="5.25"/>
<rect x="109.27" y="131.85" width="5.02" height="5.21"/>
<rect x="109.27" y="121.36" width="5.02" height="5.25"/>
<rect x="109.27" y="110.9" width="5.02" height="5.25"/>
<rect x="109.27" y="100.45" width="5.02" height="5.25"/>
<rect x="109.27" y="89.99" width="5.02" height="5.21"/>
<rect x="109.27" y="48.14" width="5.02" height="5.21"/>
<rect x="109.27" y="37.68" width="5.02" height="5.21"/>
<rect x="109.27" y="27.19" width="5.02" height="5.25"/>
<rect x="104.21" y="131.85" width="5.06" height="5.21"/>
<rect x="104.21" y="121.36" width="5.06" height="5.25"/>
<rect x="104.21" y="110.9" width="5.06" height="5.25"/>
<rect x="104.21" y="100.45" width="5.06" height="5.25"/>
<rect x="104.21" y="89.99" width="5.06" height="5.21"/>
<rect x="104.21" y="58.59" width="5.06" height="5.25"/>
<rect x="104.21" y="48.14" width="5.06" height="5.21"/>
<rect x="104.21" y="37.68" width="5.06" height="5.21"/>
<rect x="104.21" y="27.19" width="5.06" height="5.25"/>
<rect x="104.21" y="16.74" width="5.06" height="5.25"/>
<rect x="99.18" y="142.3" width="5.02" height="5.25"/>
<rect x="99.18" y="131.85" width="5.02" height="5.21"/>
<rect x="99.18" y="121.36" width="5.02" height="5.25"/>
<rect x="99.18" y="110.9" width="5.02" height="5.25"/>
<rect x="99.18" y="100.45" width="5.02" height="5.25"/>
<rect x="99.18" y="89.99" width="5.02" height="5.21"/>
<rect x="99.18" y="79.54" width="5.02" height="5.21"/>
<rect x="99.18" y="58.59" width="5.02" height="5.25"/>
<rect x="99.18" y="48.14" width="5.02" height="5.21"/>
<rect x="99.18" y="37.68" width="5.02" height="5.21"/>
<rect x="99.18" y="27.19" width="5.02" height="5.25"/>
<rect x="99.18" y="16.74" width="5.02" height="5.25"/>
<rect x="94.16" y="142.3" width="5.02" height="5.25"/>
<rect x="94.16" y="131.85" width="5.02" height="5.21"/>
<rect x="94.16" y="121.36" width="5.02" height="5.25"/>
<rect x="94.16" y="110.9" width="5.02" height="5.25"/>
<rect x="94.16" y="100.45" width="5.02" height="5.25"/>
<rect x="94.16" y="89.99" width="5.02" height="5.21"/>
<rect x="94.16" y="79.54" width="5.02" height="5.21"/>
<rect x="94.16" y="69.05" width="5.02" height="5.25"/>
<rect x="94.16" y="58.59" width="5.02" height="5.25"/>
<rect x="94.16" y="48.14" width="5.02" height="5.21"/>
<rect x="94.16" y="37.68" width="5.02" height="5.21"/>
<rect x="94.16" y="27.19" width="5.02" height="5.25"/>
<rect x="94.16" y="16.74" width="5.02" height="5.25"/>
<rect x="94.16" y="6.28" width="5.02" height="5.25"/>
<rect x="89.13" y="142.3" width="5.02" height="5.25"/>
<rect x="89.13" y="131.85" width="5.02" height="5.21"/>
<rect x="89.13" y="121.36" width="5.02" height="5.25"/>
<rect x="89.13" y="110.9" width="5.02" height="5.25"/>
<rect x="89.13" y="100.45" width="5.02" height="5.25"/>
<rect x="89.13" y="89.99" width="5.02" height="5.21"/>
<rect x="89.13" y="79.54" width="5.02" height="5.21"/>
<rect x="89.13" y="69.05" width="5.02" height="5.25"/>
<rect x="89.13" y="58.59" width="5.02" height="5.25"/>
<rect x="89.13" y="48.14" width="5.02" height="5.21"/>
<rect x="89.13" y="37.68" width="5.02" height="5.21"/>
<rect x="89.13" y="27.19" width="5.02" height="5.25"/>
<rect x="89.13" y="16.74" width="5.02" height="5.25"/>
<rect x="89.13" y="6.28" width="5.02" height="5.25"/>
<rect x="84.11" y="142.3" width="5.02" height="5.25"/>
<rect x="84.11" y="131.85" width="5.02" height="5.21"/>
<rect x="84.11" y="121.36" width="5.02" height="5.25"/>
<rect x="84.11" y="89.99" width="5.02" height="5.21"/>
<rect x="84.11" y="79.54" width="5.02" height="5.21"/>
<rect x="84.11" y="69.05" width="5.02" height="5.25"/>
<rect x="84.11" y="58.59" width="5.02" height="5.25"/>
<rect x="84.11" y="48.14" width="5.02" height="5.21"/>
<rect x="84.11" y="37.68" width="5.02" height="5.21"/>
<rect x="84.11" y="27.19" width="5.02" height="5.25"/>
<rect x="84.11" y="16.74" width="5.02" height="5.25"/>
<rect x="84.11" y="6.28" width="5.02" height="5.25"/>
<rect x="79.09" y="142.3" width="5.02" height="5.25"/>
<rect x="79.09" y="131.85" width="5.02" height="5.21"/>
<rect x="79.09" y="79.54" width="5.02" height="5.21"/>
<rect x="79.09" y="69.05" width="5.02" height="5.25"/>
<rect x="79.09" y="58.59" width="5.02" height="5.25"/>
<rect x="79.09" y="27.19" width="5.02" height="5.25"/>
<rect x="79.09" y="16.74" width="5.02" height="5.25"/>
<rect x="79.09" y="6.28" width="5.02" height="5.25"/>
<rect x="74.06" y="142.3" width="5.02" height="5.25"/>
<rect x="74.06" y="131.85" width="5.02" height="5.21"/>
<rect x="74.06" y="79.54" width="5.02" height="5.21"/>
<rect x="74.06" y="69.05" width="5.02" height="5.25"/>
<rect x="74.06" y="16.74" width="5.02" height="5.25"/>
<rect x="74.06" y="6.28" width="5.02" height="5.25"/>
<rect x="69.04" y="142.3" width="5.02" height="5.25"/>
<rect x="69.04" y="131.85" width="5.02" height="5.21"/>
<rect x="69.04" y="79.54" width="5.02" height="5.21"/>
<rect x="69.04" y="69.05" width="5.02" height="5.25"/>
<rect x="69.04" y="16.74" width="5.02" height="5.25"/>
<rect x="69.04" y="6.28" width="5.02" height="5.25"/>
<rect x="64.01" y="142.3" width="5.02" height="5.25"/>
<rect x="64.01" y="131.85" width="5.02" height="5.21"/>
<rect x="64.01" y="79.54" width="5.02" height="5.21"/>
<rect x="64.01" y="69.05" width="5.02" height="5.25"/>
<rect x="64.01" y="16.74" width="5.02" height="5.25"/>
<rect x="64.01" y="6.28" width="5.02" height="5.25"/>
<rect x="58.99" y="142.3" width="5.02" height="5.25"/>
<rect x="58.99" y="131.85" width="5.02" height="5.21"/>
<rect x="58.99" y="79.54" width="5.02" height="5.21"/>
<rect x="58.99" y="69.05" width="5.02" height="5.25"/>
<rect x="58.99" y="16.74" width="5.02" height="5.25"/>
<rect x="58.99" y="6.28" width="5.02" height="5.25"/>
<rect x="53.96" y="142.3" width="5.02" height="5.25"/>
<rect x="53.96" y="131.85" width="5.02" height="5.21"/>
<rect x="53.96" y="79.54" width="5.02" height="5.21"/>
<rect x="53.96" y="69.05" width="5.02" height="5.25"/>
<rect x="53.96" y="16.74" width="5.02" height="5.25"/>
<rect x="53.96" y="6.28" width="5.02" height="5.25"/>
<rect x="48.94" y="142.3" width="5.02" height="5.25"/>
<rect x="48.94" y="131.85" width="5.02" height="5.21"/>
<rect x="48.94" y="79.54" width="5.02" height="5.21"/>
<rect x="48.94" y="69.05" width="5.02" height="5.25"/>
<rect x="48.94" y="16.74" width="5.02" height="5.25"/>
<rect x="48.94" y="6.28" width="5.02" height="5.25"/>
<rect x="43.92" y="142.3" width="5.02" height="5.25"/>
<rect x="43.92" y="131.85" width="5.02" height="5.21"/>
<rect x="43.92" y="79.54" width="5.02" height="5.21"/>
<rect x="43.92" y="69.05" width="5.02" height="5.25"/>
<rect x="43.92" y="16.74" width="5.02" height="5.25"/>
<rect x="43.92" y="6.28" width="5.02" height="5.25"/>
<rect x="38.89" y="142.3" width="5.02" height="5.25"/>
<rect x="38.89" y="131.85" width="5.02" height="5.21"/>
<rect x="38.89" y="121.36" width="5.02" height="5.25"/>
<rect x="38.89" y="110.9" width="5.02" height="5.25"/>
<rect x="38.89" y="100.45" width="5.02" height="5.25"/>
<rect x="38.89" y="89.99" width="5.02" height="5.21"/>
<rect x="38.89" y="79.54" width="5.02" height="5.21"/>
<rect x="38.89" y="69.05" width="5.02" height="5.25"/>
<rect x="38.89" y="58.59" width="5.02" height="5.25"/>
<rect x="38.89" y="48.14" width="5.02" height="5.21"/>
<rect x="38.89" y="37.68" width="5.02" height="5.21"/>
<rect x="38.89" y="27.19" width="5.02" height="5.25"/>
<rect x="38.89" y="16.74" width="5.02" height="5.25"/>
<rect x="38.89" y="6.28" width="5.02" height="5.25"/>
<rect x="33.87" y="142.3" width="5.02" height="5.25"/>
<rect x="33.87" y="131.85" width="5.02" height="5.21"/>
<rect x="33.87" y="121.36" width="5.02" height="5.25"/>
<rect x="33.87" y="110.9" width="5.02" height="5.25"/>
<rect x="33.87" y="100.45" width="5.02" height="5.25"/>
<rect x="33.87" y="89.99" width="5.02" height="5.21"/>
<rect x="33.87" y="79.54" width="5.02" height="5.21"/>
<rect x="33.87" y="69.05" width="5.02" height="5.25"/>
<rect x="33.87" y="58.59" width="5.02" height="5.25"/>
<rect x="33.87" y="48.14" width="5.02" height="5.21"/>
<rect x="33.87" y="37.68" width="5.02" height="5.21"/>
<rect x="33.87" y="27.19" width="5.02" height="5.25"/>
<rect x="33.87" y="16.74" width="5.02" height="5.25"/>
<rect x="33.87" y="6.28" width="5.02" height="5.25"/>
<rect x="28.84" y="142.3" width="5.02" height="5.25"/>
<rect x="28.84" y="131.85" width="5.02" height="5.21"/>
<rect x="28.84" y="121.36" width="5.02" height="5.25"/>
<rect x="28.84" y="110.9" width="5.02" height="5.25"/>
<rect x="28.84" y="100.45" width="5.02" height="5.25"/>
<rect x="28.84" y="89.99" width="5.02" height="5.21"/>
<rect x="28.84" y="79.54" width="5.02" height="5.21"/>
<rect x="28.84" y="69.05" width="5.02" height="5.25"/>
<rect x="28.84" y="58.59" width="5.02" height="5.25"/>
<rect x="28.84" y="48.14" width="5.02" height="5.21"/>
<rect x="28.84" y="37.68" width="5.02" height="5.21"/>
<rect x="28.84" y="27.19" width="5.02" height="5.25"/>
<rect x="28.84" y="16.74" width="5.02" height="5.25"/>
<rect x="28.84" y="6.28" width="5.02" height="5.25"/>
<rect x="23.78" y="142.3" width="5.06" height="5.25"/>
<rect x="23.78" y="131.85" width="5.06" height="5.21"/>
<rect x="23.78" y="121.36" width="5.06" height="5.25"/>
<rect x="23.78" y="110.9" width="5.06" height="5.25"/>
<rect x="23.78" y="100.45" width="5.06" height="5.25"/>
<rect x="23.78" y="89.99" width="5.06" height="5.21"/>
<rect x="23.78" y="79.54" width="5.06" height="5.21"/>
<rect x="23.78" y="69.05" width="5.06" height="5.25"/>
<rect x="23.78" y="58.59" width="5.06" height="5.25"/>
<rect x="23.78" y="48.14" width="5.06" height="5.21"/>
<rect x="23.78" y="37.68" width="5.06" height="5.21"/>
<rect x="23.78" y="27.19" width="5.06" height="5.25"/>
<rect x="23.78" y="16.74" width="5.06" height="5.25"/>
<rect x="23.78" y="6.28" width="5.06" height="5.25"/>
<rect x="18.76" y="142.3" width="5.02" height="5.25"/>
<rect x="18.76" y="131.85" width="5.02" height="5.21"/>
<rect x="18.76" y="121.36" width="5.02" height="5.25"/>
<rect x="18.76" y="110.9" width="5.02" height="5.25"/>
<rect x="18.76" y="100.45" width="5.02" height="5.25"/>
<rect x="18.76" y="89.99" width="5.02" height="5.21"/>
<rect x="18.76" y="79.54" width="5.02" height="5.21"/>
<rect x="18.76" y="69.05" width="5.02" height="5.25"/>
<rect x="18.76" y="58.59" width="5.02" height="5.25"/>
<rect x="18.76" y="48.14" width="5.02" height="5.21"/>
<rect x="18.76" y="37.68" width="5.02" height="5.21"/>
<rect x="18.76" y="27.19" width="5.02" height="5.25"/>
<rect x="18.76" y="16.74" width="5.02" height="5.25"/>
<rect x="18.76" y="6.28" width="5.02" height="5.25"/>
<rect x="13.73" y="142.3" width="5.02" height="5.25"/>
<rect x="13.73" y="131.85" width="5.02" height="5.21"/>
<rect x="13.73" y="121.36" width="5.02" height="5.25"/>
<rect x="13.73" y="110.9" width="5.02" height="5.25"/>
<rect x="13.73" y="100.45" width="5.02" height="5.25"/>
<rect x="13.73" y="89.99" width="5.02" height="5.21"/>
<rect x="13.73" y="79.54" width="5.02" height="5.21"/>
<rect x="13.73" y="69.05" width="5.02" height="5.25"/>
<rect x="13.73" y="58.59" width="5.02" height="5.25"/>
<rect x="13.73" y="48.14" width="5.02" height="5.21"/>
<rect x="13.73" y="37.68" width="5.02" height="5.21"/>
<rect x="13.73" y="27.19" width="5.02" height="5.25"/>
<rect x="13.73" y="16.74" width="5.02" height="5.25"/>
<rect x="13.73" y="6.28" width="5.02" height="5.25"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,243 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129.61 163.63">
<defs>
<style>
.cls-1 {
fill: #ffdf07;
}
.cls-2 {
fill: #575756;
}
</style>
</defs>
<g id="Livello_1-2" data-name="Livello 1">
<g>
<rect class="cls-2" y="157.08" width="125.66" height="1.05"/>
<rect class="cls-2" y="146.6" width="125.66" height="1.05"/>
<rect class="cls-2" y="136.13" width="125.66" height="1.05"/>
<rect class="cls-2" y="125.66" width="125.66" height="1.05"/>
<rect class="cls-2" y="115.19" width="125.66" height="1.05"/>
<rect class="cls-2" y="104.72" width="125.66" height="1.05"/>
<rect class="cls-2" y="94.25" width="125.66" height="1.05"/>
<rect class="cls-2" y="83.77" width="125.66" height="1.05"/>
<rect class="cls-2" y="73.3" width="125.66" height="1.05"/>
<rect class="cls-2" y="62.83" width="125.66" height="1.05"/>
<rect class="cls-2" y="52.36" width="125.66" height="1.05"/>
<rect class="cls-2" y="41.89" width="125.66" height="1.05"/>
<rect class="cls-2" y="31.42" width="125.66" height="1.05"/>
<rect class="cls-2" y="20.94" width="125.66" height="1.05"/>
<rect class="cls-2" y="10.47" width="125.66" height="1.05"/>
<rect class="cls-2" width="125.66" height="1.05"/>
</g>
<g>
<rect class="cls-1" x="114.27" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="114.27" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="114.27" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="109.24" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="109.24" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="109.24" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="109.24" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="109.24" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="109.24" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="109.24" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="109.24" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="104.22" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="104.22" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="104.22" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="104.22" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="104.22" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="104.22" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="104.22" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="104.22" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="104.22" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="104.22" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="99.19" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="99.19" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="99.19" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="99.19" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="99.19" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="99.19" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="99.19" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="99.19" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="99.19" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="99.19" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="99.19" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="99.19" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="94.17" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="94.17" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="94.17" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="94.17" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="94.17" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="94.17" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="94.17" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="94.17" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="94.17" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="94.17" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="94.17" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="94.17" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="94.17" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="94.17" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="89.15" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="89.15" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="89.15" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="89.15" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="89.15" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="89.15" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="89.15" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="89.15" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="89.15" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="89.15" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="89.15" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="89.15" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="89.15" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="89.15" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="84.12" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="84.12" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="84.12" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="84.12" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="84.12" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="84.12" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="84.12" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="84.12" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="84.12" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="84.12" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="84.12" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="84.12" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="79.1" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="79.1" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="79.1" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="79.1" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="79.1" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="79.1" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="79.1" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="79.1" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="74.07" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="74.07" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="74.07" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="74.07" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="74.07" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="74.07" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="69.05" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="69.05" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="69.05" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="69.05" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="69.05" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="69.05" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="64.03" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="64.03" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="64.03" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="64.03" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="64.03" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="64.03" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="59" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="59" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="59" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="59" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="59" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="59" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="53.98" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="53.98" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="53.98" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="53.98" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="53.98" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="53.98" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="48.92" y="142.45" width="5.06" height="5.25"/>
<rect class="cls-1" x="48.92" y="131.99" width="5.06" height="5.21"/>
<rect class="cls-1" x="48.92" y="79.68" width="5.06" height="5.21"/>
<rect class="cls-1" x="48.92" y="69.19" width="5.06" height="5.25"/>
<rect class="cls-1" x="48.92" y="16.88" width="5.06" height="5.25"/>
<rect class="cls-1" x="48.92" y="6.43" width="5.06" height="5.25"/>
<rect class="cls-1" x="43.89" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="43.89" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="43.89" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="43.89" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="43.89" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="43.89" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="38.87" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="38.87" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="38.87" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="38.87" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="38.87" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="38.87" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="38.87" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="38.87" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="38.87" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="38.87" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="38.87" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="38.87" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="38.87" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="38.87" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="33.84" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="33.84" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="33.84" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="33.84" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="33.84" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="33.84" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="33.84" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="33.84" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="33.84" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="33.84" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="33.84" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="33.84" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="33.84" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="33.84" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="28.82" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="28.82" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="28.82" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="28.82" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="28.82" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="28.82" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="28.82" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="28.82" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="28.82" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="28.82" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="28.82" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="28.82" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="28.82" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="28.82" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="23.8" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="23.8" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="23.8" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="23.8" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="23.8" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="23.8" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="23.8" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="23.8" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="23.8" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="23.8" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="23.8" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="23.8" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="23.8" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="23.8" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="18.77" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="18.77" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="18.77" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="18.77" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="18.77" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="18.77" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="18.77" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="18.77" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="18.77" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="18.77" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="18.77" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="18.77" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="18.77" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="18.77" y="6.43" width="5.02" height="5.25"/>
<rect class="cls-1" x="13.75" y="142.45" width="5.02" height="5.25"/>
<rect class="cls-1" x="13.75" y="131.99" width="5.02" height="5.21"/>
<rect class="cls-1" x="13.75" y="121.54" width="5.02" height="5.21"/>
<rect class="cls-1" x="13.75" y="111.05" width="5.02" height="5.25"/>
<rect class="cls-1" x="13.75" y="100.59" width="5.02" height="5.25"/>
<rect class="cls-1" x="13.75" y="90.14" width="5.02" height="5.21"/>
<rect class="cls-1" x="13.75" y="79.68" width="5.02" height="5.21"/>
<rect class="cls-1" x="13.75" y="69.19" width="5.02" height="5.25"/>
<rect class="cls-1" x="13.75" y="58.74" width="5.02" height="5.25"/>
<rect class="cls-1" x="13.75" y="48.28" width="5.02" height="5.25"/>
<rect class="cls-1" x="13.75" y="37.83" width="5.02" height="5.21"/>
<rect class="cls-1" x="13.75" y="27.34" width="5.02" height="5.25"/>
<rect class="cls-1" x="13.75" y="16.88" width="5.02" height="5.25"/>
<rect class="cls-1" x="13.75" y="6.43" width="5.02" height="5.25"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.16 163.48">
<defs>
<style>
.cls-1 {
fill: #1d1d1b;
}
.cls-2 {
fill: #575756;
}
</style>
</defs>
<g id="Livello_1-2" data-name="Livello 1">
<g>
<rect class="cls-2" x=".25" y="157.08" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="146.6" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="136.13" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="125.66" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="115.19" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="104.72" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="94.25" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="83.77" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="73.3" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="62.83" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="52.36" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="41.89" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="31.42" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="20.94" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" y="10.47" width="125.66" height="1.05"/>
<rect class="cls-2" x=".25" width="125.66" height="1.05"/>
</g>
<g>
<rect class="cls-1" x="110.84" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.84" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.84" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="110.84" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="110.84" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.84" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.84" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="110.84" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="110.84" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.84" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.82" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.82" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.82" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.82" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="105.82" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="105.82" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.82" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.82" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="105.82" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="105.82" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.82" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.82" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.79" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.79" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.79" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.79" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.79" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.79" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.79" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.79" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.79" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.79" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.79" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.79" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.77" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.77" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.77" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.77" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.77" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.77" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.77" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.77" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.77" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.77" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.77" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.77" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.77" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.77" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.74" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.74" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.74" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.74" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.74" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.74" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.74" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.74" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.74" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.74" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.74" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.74" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.74" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.74" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="85.68" y="142.31" width="5.06" height="5.21"/>
<rect class="cls-1" x="85.68" y="131.82" width="5.06" height="5.25"/>
<rect class="cls-1" x="85.68" y="121.37" width="5.06" height="5.25"/>
<rect class="cls-1" x="85.68" y="110.91" width="5.06" height="5.25"/>
<rect class="cls-1" x="85.68" y="100.46" width="5.06" height="5.21"/>
<rect class="cls-1" x="85.68" y="90" width="5.06" height="5.21"/>
<rect class="cls-1" x="85.68" y="79.51" width="5.06" height="5.25"/>
<rect class="cls-1" x="85.68" y="69.06" width="5.06" height="5.25"/>
<rect class="cls-1" x="85.68" y="58.6" width="5.06" height="5.21"/>
<rect class="cls-1" x="85.68" y="48.15" width="5.06" height="5.21"/>
<rect class="cls-1" x="85.68" y="37.65" width="5.06" height="5.25"/>
<rect class="cls-1" x="85.68" y="27.2" width="5.06" height="5.25"/>
<rect class="cls-1" x="85.68" y="16.75" width="5.06" height="5.25"/>
<rect class="cls-1" x="85.68" y="6.29" width="5.06" height="5.21"/>
<rect class="cls-1" x="80.66" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="80.66" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.66" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.66" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.66" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.66" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.66" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.66" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="75.63" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="75.63" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.63" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.63" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.63" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.63" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="70.61" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="70.61" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.61" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.61" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.61" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.61" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="65.59" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="65.59" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="65.59" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="65.59" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="65.59" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="65.59" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="60.56" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="60.56" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.56" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.56" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.56" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.56" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="55.54" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="55.54" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.54" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.54" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.54" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.54" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="50.51" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="50.51" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.51" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.51" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.51" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.51" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="45.49" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="45.49" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="45.49" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="45.49" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="45.49" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="45.49" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="40.47" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="40.47" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.47" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.47" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.47" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.47" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.44" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.44" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.44" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.44" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.44" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.44" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.44" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.44" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.44" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.44" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.44" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.44" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.44" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.44" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.42" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.42" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.42" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.42" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.42" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.42" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.42" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.42" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.42" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.42" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.42" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.42" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.42" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.42" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.39" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.39" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.39" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.39" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.39" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.39" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.39" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.39" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.39" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.39" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.39" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.39" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.39" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.39" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.37" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.37" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.37" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.37" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.37" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.37" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.37" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.37" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.37" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.37" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.37" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.37" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.37" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.37" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.35" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.35" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.35" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.35" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.35" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.35" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.35" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.35" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.35" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.35" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.35" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.35" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.35" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.35" y="6.29" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.32" y="142.31" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.32" y="131.82" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.32" y="121.37" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.32" y="110.91" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.32" y="100.46" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.32" y="90" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.32" y="79.51" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.32" y="69.06" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.32" y="58.6" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.32" y="48.15" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.32" y="37.65" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.32" y="27.2" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.32" y="16.75" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.32" y="6.29" width="5.02" height="5.21"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.16 163.48">
<g id="Livello_1-2" data-name="Livello 1">
<g>
<rect x=".25" y="157.08" width="125.66" height="1.05"/>
<rect x=".25" y="146.6" width="125.66" height="1.05"/>
<rect x=".25" y="136.13" width="125.66" height="1.05"/>
<rect x=".25" y="125.66" width="125.66" height="1.05"/>
<rect x=".25" y="115.19" width="125.66" height="1.05"/>
<rect x=".25" y="104.72" width="125.66" height="1.05"/>
<rect x=".25" y="94.25" width="125.66" height="1.05"/>
<rect x=".25" y="83.77" width="125.66" height="1.05"/>
<rect x=".25" y="73.3" width="125.66" height="1.05"/>
<rect x=".25" y="62.83" width="125.66" height="1.05"/>
<rect x=".25" y="52.36" width="125.66" height="1.05"/>
<rect x=".25" y="41.89" width="125.66" height="1.05"/>
<rect x=".25" y="31.42" width="125.66" height="1.05"/>
<rect x=".25" y="20.94" width="125.66" height="1.05"/>
<rect x=".25" y="10.47" width="125.66" height="1.05"/>
<rect x=".25" width="125.66" height="1.05"/>
</g>
<g>
<rect x="110.85" y="27.19" width="5.02" height="5.25"/>
<rect x="110.85" y="16.73" width="5.02" height="5.25"/>
<rect x="110.85" y="6.28" width="5.02" height="5.25"/>
<rect x="105.82" y="27.19" width="5.02" height="5.25"/>
<rect x="105.82" y="16.73" width="5.02" height="5.25"/>
<rect x="105.82" y="6.28" width="5.02" height="5.25"/>
<rect x="100.8" y="27.19" width="5.02" height="5.25"/>
<rect x="100.8" y="16.73" width="5.02" height="5.25"/>
<rect x="100.8" y="6.28" width="5.02" height="5.25"/>
<rect x="95.74" y="27.19" width="5.06" height="5.25"/>
<rect x="95.74" y="16.73" width="5.06" height="5.25"/>
<rect x="95.74" y="6.28" width="5.06" height="5.25"/>
<rect x="90.71" y="27.19" width="5.02" height="5.25"/>
<rect x="90.71" y="16.73" width="5.02" height="5.25"/>
<rect x="90.71" y="6.28" width="5.02" height="5.25"/>
<rect x="85.69" y="27.19" width="5.02" height="5.25"/>
<rect x="85.69" y="16.73" width="5.02" height="5.25"/>
<rect x="85.69" y="6.28" width="5.02" height="5.25"/>
<rect x="80.66" y="27.19" width="5.02" height="5.25"/>
<rect x="80.66" y="16.73" width="5.02" height="5.25"/>
<rect x="80.66" y="6.28" width="5.02" height="5.25"/>
<rect x="75.64" y="69.04" width="5.02" height="5.25"/>
<rect x="75.64" y="58.59" width="5.02" height="5.25"/>
<rect x="75.64" y="27.19" width="5.02" height="5.25"/>
<rect x="75.64" y="16.73" width="5.02" height="5.25"/>
<rect x="75.64" y="6.28" width="5.02" height="5.25"/>
<rect x="70.62" y="69.04" width="5.02" height="5.25"/>
<rect x="70.62" y="58.59" width="5.02" height="5.25"/>
<rect x="70.62" y="27.19" width="5.02" height="5.25"/>
<rect x="70.62" y="16.73" width="5.02" height="5.25"/>
<rect x="70.62" y="6.28" width="5.02" height="5.25"/>
<rect x="65.59" y="69.04" width="5.02" height="5.25"/>
<rect x="65.59" y="58.59" width="5.02" height="5.25"/>
<rect x="65.59" y="27.19" width="5.02" height="5.25"/>
<rect x="65.59" y="16.73" width="5.02" height="5.25"/>
<rect x="65.59" y="6.28" width="5.02" height="5.25"/>
<rect x="60.57" y="69.04" width="5.02" height="5.25"/>
<rect x="60.57" y="58.59" width="5.02" height="5.25"/>
<rect x="60.57" y="27.19" width="5.02" height="5.25"/>
<rect x="60.57" y="16.73" width="5.02" height="5.25"/>
<rect x="60.57" y="6.28" width="5.02" height="5.25"/>
<rect x="55.54" y="69.04" width="5.02" height="5.25"/>
<rect x="55.54" y="58.59" width="5.02" height="5.25"/>
<rect x="55.54" y="27.19" width="5.02" height="5.25"/>
<rect x="55.54" y="16.73" width="5.02" height="5.25"/>
<rect x="55.54" y="6.28" width="5.02" height="5.25"/>
<rect x="50.52" y="69.04" width="5.02" height="5.25"/>
<rect x="50.52" y="58.59" width="5.02" height="5.25"/>
<rect x="50.52" y="27.19" width="5.02" height="5.25"/>
<rect x="50.52" y="16.73" width="5.02" height="5.25"/>
<rect x="50.52" y="6.28" width="5.02" height="5.25"/>
<rect x="45.49" y="69.04" width="5.02" height="5.25"/>
<rect x="45.49" y="58.59" width="5.02" height="5.25"/>
<rect x="45.49" y="27.19" width="5.02" height="5.25"/>
<rect x="45.49" y="16.73" width="5.02" height="5.25"/>
<rect x="45.49" y="6.28" width="5.02" height="5.25"/>
<rect x="40.47" y="69.04" width="5.02" height="5.25"/>
<rect x="40.47" y="58.59" width="5.02" height="5.25"/>
<rect x="40.47" y="27.19" width="5.02" height="5.25"/>
<rect x="40.47" y="16.73" width="5.02" height="5.25"/>
<rect x="40.47" y="6.28" width="5.02" height="5.25"/>
<rect x="35.45" y="142.3" width="5.02" height="5.25"/>
<rect x="35.45" y="131.85" width="5.02" height="5.21"/>
<rect x="35.45" y="121.39" width="5.02" height="5.21"/>
<rect x="35.45" y="110.9" width="5.02" height="5.25"/>
<rect x="35.45" y="100.44" width="5.02" height="5.25"/>
<rect x="35.45" y="89.99" width="5.02" height="5.21"/>
<rect x="35.45" y="79.54" width="5.02" height="5.21"/>
<rect x="35.45" y="69.04" width="5.02" height="5.25"/>
<rect x="35.45" y="58.59" width="5.02" height="5.25"/>
<rect x="35.45" y="48.13" width="5.02" height="5.25"/>
<rect x="35.45" y="37.68" width="5.02" height="5.21"/>
<rect x="35.45" y="27.19" width="5.02" height="5.25"/>
<rect x="35.45" y="16.73" width="5.02" height="5.25"/>
<rect x="35.45" y="6.28" width="5.02" height="5.25"/>
<rect x="30.42" y="142.3" width="5.02" height="5.25"/>
<rect x="30.42" y="131.85" width="5.02" height="5.21"/>
<rect x="30.42" y="121.39" width="5.02" height="5.21"/>
<rect x="30.42" y="110.9" width="5.02" height="5.25"/>
<rect x="30.42" y="100.44" width="5.02" height="5.25"/>
<rect x="30.42" y="89.99" width="5.02" height="5.21"/>
<rect x="30.42" y="79.54" width="5.02" height="5.21"/>
<rect x="30.42" y="69.04" width="5.02" height="5.25"/>
<rect x="30.42" y="58.59" width="5.02" height="5.25"/>
<rect x="30.42" y="48.13" width="5.02" height="5.25"/>
<rect x="30.42" y="37.68" width="5.02" height="5.21"/>
<rect x="30.42" y="27.19" width="5.02" height="5.25"/>
<rect x="30.42" y="16.73" width="5.02" height="5.25"/>
<rect x="30.42" y="6.28" width="5.02" height="5.25"/>
<rect x="25.4" y="142.3" width="5.02" height="5.25"/>
<rect x="25.4" y="131.85" width="5.02" height="5.21"/>
<rect x="25.4" y="121.39" width="5.02" height="5.21"/>
<rect x="25.4" y="110.9" width="5.02" height="5.25"/>
<rect x="25.4" y="100.44" width="5.02" height="5.25"/>
<rect x="25.4" y="89.99" width="5.02" height="5.21"/>
<rect x="25.4" y="79.54" width="5.02" height="5.21"/>
<rect x="25.4" y="69.04" width="5.02" height="5.25"/>
<rect x="25.4" y="58.59" width="5.02" height="5.25"/>
<rect x="25.4" y="48.13" width="5.02" height="5.25"/>
<rect x="25.4" y="37.68" width="5.02" height="5.21"/>
<rect x="25.4" y="27.19" width="5.02" height="5.25"/>
<rect x="25.4" y="16.73" width="5.02" height="5.25"/>
<rect x="25.4" y="6.28" width="5.02" height="5.25"/>
<rect x="20.34" y="142.3" width="5.06" height="5.25"/>
<rect x="20.34" y="131.85" width="5.06" height="5.21"/>
<rect x="20.34" y="121.39" width="5.06" height="5.21"/>
<rect x="20.34" y="110.9" width="5.06" height="5.25"/>
<rect x="20.34" y="100.44" width="5.06" height="5.25"/>
<rect x="20.34" y="89.99" width="5.06" height="5.21"/>
<rect x="20.34" y="79.54" width="5.06" height="5.21"/>
<rect x="20.34" y="69.04" width="5.06" height="5.25"/>
<rect x="20.34" y="58.59" width="5.06" height="5.25"/>
<rect x="20.34" y="48.13" width="5.06" height="5.25"/>
<rect x="20.34" y="37.68" width="5.06" height="5.21"/>
<rect x="20.34" y="27.19" width="5.06" height="5.25"/>
<rect x="20.34" y="16.73" width="5.06" height="5.25"/>
<rect x="20.34" y="6.28" width="5.06" height="5.25"/>
<rect x="15.31" y="142.3" width="5.02" height="5.25"/>
<rect x="15.31" y="131.85" width="5.02" height="5.21"/>
<rect x="15.31" y="121.39" width="5.02" height="5.21"/>
<rect x="15.31" y="110.9" width="5.02" height="5.25"/>
<rect x="15.31" y="100.44" width="5.02" height="5.25"/>
<rect x="15.31" y="89.99" width="5.02" height="5.21"/>
<rect x="15.31" y="79.54" width="5.02" height="5.21"/>
<rect x="15.31" y="69.04" width="5.02" height="5.25"/>
<rect x="15.31" y="58.59" width="5.02" height="5.25"/>
<rect x="15.31" y="48.13" width="5.02" height="5.25"/>
<rect x="15.31" y="37.68" width="5.02" height="5.21"/>
<rect x="15.31" y="27.19" width="5.02" height="5.25"/>
<rect x="15.31" y="16.73" width="5.02" height="5.25"/>
<rect x="15.31" y="6.28" width="5.02" height="5.25"/>
<rect x="10.29" y="142.3" width="5.02" height="5.25"/>
<rect x="10.29" y="131.85" width="5.02" height="5.21"/>
<rect x="10.29" y="121.39" width="5.02" height="5.21"/>
<rect x="10.29" y="110.9" width="5.02" height="5.25"/>
<rect x="10.29" y="100.44" width="5.02" height="5.25"/>
<rect x="10.29" y="89.99" width="5.02" height="5.21"/>
<rect x="10.29" y="79.54" width="5.02" height="5.21"/>
<rect x="10.29" y="69.04" width="5.02" height="5.25"/>
<rect x="10.29" y="58.59" width="5.02" height="5.25"/>
<rect x="10.29" y="48.13" width="5.02" height="5.25"/>
<rect x="10.29" y="37.68" width="5.02" height="5.21"/>
<rect x="10.29" y="27.19" width="5.02" height="5.25"/>
<rect x="10.29" y="16.73" width="5.02" height="5.25"/>
<rect x="10.29" y="6.28" width="5.02" height="5.25"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -0,0 +1,181 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.16 163.62">
<defs>
<style>
.cls-1 {
fill: #ffdf07;
}
.cls-2 {
fill: #575756;
}
</style>
</defs>
<g id="Livello_1-2" data-name="Livello 1">
<g>
<rect class="cls-2" x=".27" y="157.08" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="146.6" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="136.13" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="125.66" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="115.19" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="104.72" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="94.25" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="83.77" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="73.3" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="62.83" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="52.36" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="41.89" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="31.42" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="20.94" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" y="10.47" width="125.66" height="1.05"/>
<rect class="cls-2" x=".27" width="125.66" height="1.05"/>
</g>
<g>
<rect class="cls-1" x="110.82" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="110.82" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="110.82" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.8" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="105.8" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="105.8" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.77" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="100.77" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="100.77" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.75" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="95.75" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="95.75" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.72" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="90.72" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="90.72" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="85.7" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="85.7" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="85.7" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.68" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="80.68" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="80.68" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.65" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.65" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.65" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="75.65" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="75.65" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.63" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.63" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.63" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="70.63" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="70.63" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="65.6" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="65.6" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="65.6" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="65.6" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="65.6" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.58" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.58" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.58" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="60.58" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="60.58" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.56" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.56" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.56" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="55.56" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="55.56" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.53" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.53" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.53" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="50.53" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="50.53" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="45.47" y="69.18" width="5.06" height="5.25"/>
<rect class="cls-1" x="45.47" y="58.73" width="5.06" height="5.25"/>
<rect class="cls-1" x="45.47" y="27.36" width="5.06" height="5.21"/>
<rect class="cls-1" x="45.47" y="16.87" width="5.06" height="5.25"/>
<rect class="cls-1" x="45.47" y="6.42" width="5.06" height="5.25"/>
<rect class="cls-1" x="40.45" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.45" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.45" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="40.45" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="40.45" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.42" y="142.44" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.42" y="131.98" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.42" y="121.53" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.42" y="111.04" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.42" y="100.58" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.42" y="90.13" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.42" y="79.67" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.42" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.42" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.42" y="48.27" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.42" y="37.82" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.42" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="35.42" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="35.42" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.4" y="142.44" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.4" y="131.98" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.4" y="121.53" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.4" y="111.04" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.4" y="100.58" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.4" y="90.13" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.4" y="79.67" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.4" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.4" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.4" y="48.27" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.4" y="37.82" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.4" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="30.4" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="30.4" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.37" y="142.44" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.37" y="131.98" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.37" y="121.53" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.37" y="111.04" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.37" y="100.58" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.37" y="90.13" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.37" y="79.67" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.37" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.37" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.37" y="48.27" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.37" y="37.82" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.37" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="25.37" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="25.37" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.35" y="142.44" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.35" y="131.98" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.35" y="121.53" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.35" y="111.04" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.35" y="100.58" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.35" y="90.13" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.35" y="79.67" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.35" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.35" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.35" y="48.27" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.35" y="37.82" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.35" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="20.35" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="20.35" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.33" y="142.44" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.33" y="131.98" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.33" y="121.53" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.33" y="111.04" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.33" y="100.58" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.33" y="90.13" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.33" y="79.67" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.33" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.33" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.33" y="48.27" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.33" y="37.82" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.33" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="15.33" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="15.33" y="6.42" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.3" y="142.44" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.3" y="131.98" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.3" y="121.53" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.3" y="111.04" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.3" y="100.58" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.3" y="90.13" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.3" y="79.67" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.3" y="69.18" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.3" y="58.73" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.3" y="48.27" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.3" y="37.82" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.3" y="27.36" width="5.02" height="5.21"/>
<rect class="cls-1" x="10.3" y="16.87" width="5.02" height="5.25"/>
<rect class="cls-1" x="10.3" y="6.42" width="5.02" height="5.25"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.16 163.48">
<g id="Livello_1-2" data-name="Livello 1">
<g>
<rect x=".25" y="157.08" width="125.66" height="1.05"/>
<rect x=".25" y="146.6" width="125.66" height="1.05"/>
<rect x=".25" y="136.13" width="125.66" height="1.05"/>
<rect x=".25" y="125.66" width="125.66" height="1.05"/>
<rect x=".25" y="115.19" width="125.66" height="1.05"/>
<rect x=".25" y="104.72" width="125.66" height="1.05"/>
<rect x=".25" y="94.25" width="125.66" height="1.05"/>
<rect x=".25" y="83.77" width="125.66" height="1.05"/>
<rect x=".25" y="73.3" width="125.66" height="1.05"/>
<rect x=".25" y="62.83" width="125.66" height="1.05"/>
<rect x=".25" y="52.36" width="125.66" height="1.05"/>
<rect x=".25" y="41.89" width="125.66" height="1.05"/>
<rect x=".25" y="31.42" width="125.66" height="1.05"/>
<rect x=".25" y="20.94" width="125.66" height="1.05"/>
<rect x=".25" y="10.47" width="125.66" height="1.05"/>
<rect x=".25" width="125.66" height="1.05"/>
</g>
<g>
<rect x="110.83" y="121.37" width="5.02" height="5.25"/>
<rect x="110.83" y="110.91" width="5.02" height="5.25"/>
<rect x="110.83" y="100.46" width="5.02" height="5.21"/>
<rect x="110.83" y="89.97" width="5.02" height="5.25"/>
<rect x="110.83" y="79.51" width="5.02" height="5.25"/>
<rect x="110.83" y="69.06" width="5.02" height="5.25"/>
<rect x="110.83" y="58.6" width="5.02" height="5.21"/>
<rect x="110.83" y="48.15" width="5.02" height="5.21"/>
<rect x="110.83" y="37.66" width="5.02" height="5.25"/>
<rect x="110.83" y="27.2" width="5.02" height="5.25"/>
<rect x="105.8" y="131.82" width="5.02" height="5.25"/>
<rect x="105.8" y="121.37" width="5.02" height="5.25"/>
<rect x="105.8" y="110.91" width="5.02" height="5.25"/>
<rect x="105.8" y="100.46" width="5.02" height="5.21"/>
<rect x="105.8" y="89.97" width="5.02" height="5.25"/>
<rect x="105.8" y="79.51" width="5.02" height="5.25"/>
<rect x="105.8" y="69.06" width="5.02" height="5.25"/>
<rect x="105.8" y="58.6" width="5.02" height="5.21"/>
<rect x="105.8" y="48.15" width="5.02" height="5.21"/>
<rect x="105.8" y="37.66" width="5.02" height="5.25"/>
<rect x="105.8" y="27.2" width="5.02" height="5.25"/>
<rect x="105.8" y="16.75" width="5.02" height="5.21"/>
<rect x="100.78" y="131.82" width="5.02" height="5.25"/>
<rect x="100.78" y="121.37" width="5.02" height="5.25"/>
<rect x="100.78" y="110.91" width="5.02" height="5.25"/>
<rect x="100.78" y="100.46" width="5.02" height="5.21"/>
<rect x="100.78" y="89.97" width="5.02" height="5.25"/>
<rect x="100.78" y="79.51" width="5.02" height="5.25"/>
<rect x="100.78" y="69.06" width="5.02" height="5.25"/>
<rect x="100.78" y="58.6" width="5.02" height="5.21"/>
<rect x="100.78" y="48.15" width="5.02" height="5.21"/>
<rect x="100.78" y="37.66" width="5.02" height="5.25"/>
<rect x="100.78" y="27.2" width="5.02" height="5.25"/>
<rect x="100.78" y="16.75" width="5.02" height="5.21"/>
<rect x="95.76" y="142.31" width="5.02" height="5.21"/>
<rect x="95.76" y="131.82" width="5.02" height="5.25"/>
<rect x="95.76" y="121.37" width="5.02" height="5.25"/>
<rect x="95.76" y="110.91" width="5.02" height="5.25"/>
<rect x="95.76" y="100.46" width="5.02" height="5.21"/>
<rect x="95.76" y="89.97" width="5.02" height="5.25"/>
<rect x="95.76" y="79.51" width="5.02" height="5.25"/>
<rect x="95.76" y="69.06" width="5.02" height="5.25"/>
<rect x="95.76" y="58.6" width="5.02" height="5.21"/>
<rect x="95.76" y="48.15" width="5.02" height="5.21"/>
<rect x="95.76" y="37.66" width="5.02" height="5.25"/>
<rect x="95.76" y="27.2" width="5.02" height="5.25"/>
<rect x="95.76" y="16.75" width="5.02" height="5.21"/>
<rect x="95.76" y="6.29" width="5.02" height="5.21"/>
<rect x="90.73" y="142.31" width="5.02" height="5.21"/>
<rect x="90.73" y="131.82" width="5.02" height="5.25"/>
<rect x="90.73" y="121.37" width="5.02" height="5.25"/>
<rect x="90.73" y="110.91" width="5.02" height="5.25"/>
<rect x="90.73" y="100.46" width="5.02" height="5.21"/>
<rect x="90.73" y="89.97" width="5.02" height="5.25"/>
<rect x="90.73" y="79.51" width="5.02" height="5.25"/>
<rect x="90.73" y="69.06" width="5.02" height="5.25"/>
<rect x="90.73" y="58.6" width="5.02" height="5.21"/>
<rect x="90.73" y="48.15" width="5.02" height="5.21"/>
<rect x="90.73" y="37.66" width="5.02" height="5.25"/>
<rect x="90.73" y="27.2" width="5.02" height="5.25"/>
<rect x="90.73" y="16.75" width="5.02" height="5.21"/>
<rect x="90.73" y="6.29" width="5.02" height="5.21"/>
<rect x="85.71" y="142.31" width="5.02" height="5.21"/>
<rect x="85.71" y="131.82" width="5.02" height="5.25"/>
<rect x="85.71" y="121.37" width="5.02" height="5.25"/>
<rect x="85.71" y="110.91" width="5.02" height="5.25"/>
<rect x="85.71" y="100.46" width="5.02" height="5.21"/>
<rect x="85.71" y="89.97" width="5.02" height="5.25"/>
<rect x="85.71" y="79.51" width="5.02" height="5.25"/>
<rect x="85.71" y="69.06" width="5.02" height="5.25"/>
<rect x="85.71" y="58.6" width="5.02" height="5.21"/>
<rect x="85.71" y="48.15" width="5.02" height="5.21"/>
<rect x="85.71" y="37.66" width="5.02" height="5.25"/>
<rect x="85.71" y="27.2" width="5.02" height="5.25"/>
<rect x="85.71" y="16.75" width="5.02" height="5.21"/>
<rect x="85.71" y="6.29" width="5.02" height="5.21"/>
<rect x="80.68" y="142.31" width="5.02" height="5.21"/>
<rect x="80.68" y="131.82" width="5.02" height="5.25"/>
<rect x="80.68" y="121.37" width="5.02" height="5.25"/>
<rect x="80.68" y="110.91" width="5.02" height="5.25"/>
<rect x="80.68" y="37.66" width="5.02" height="5.25"/>
<rect x="80.68" y="27.2" width="5.02" height="5.25"/>
<rect x="80.68" y="16.75" width="5.02" height="5.21"/>
<rect x="80.68" y="6.29" width="5.02" height="5.21"/>
<rect x="75.66" y="142.31" width="5.02" height="5.21"/>
<rect x="75.66" y="131.82" width="5.02" height="5.25"/>
<rect x="75.66" y="121.37" width="5.02" height="5.25"/>
<rect x="75.66" y="27.2" width="5.02" height="5.25"/>
<rect x="75.66" y="16.75" width="5.02" height="5.21"/>
<rect x="75.66" y="6.29" width="5.02" height="5.21"/>
<rect x="70.64" y="142.31" width="5.02" height="5.21"/>
<rect x="70.64" y="131.82" width="5.02" height="5.25"/>
<rect x="70.64" y="121.37" width="5.02" height="5.25"/>
<rect x="70.64" y="27.2" width="5.02" height="5.25"/>
<rect x="70.64" y="16.75" width="5.02" height="5.21"/>
<rect x="70.64" y="6.29" width="5.02" height="5.21"/>
<rect x="65.61" y="142.31" width="5.02" height="5.21"/>
<rect x="65.61" y="131.82" width="5.02" height="5.25"/>
<rect x="65.61" y="121.37" width="5.02" height="5.25"/>
<rect x="65.61" y="27.2" width="5.02" height="5.25"/>
<rect x="65.61" y="16.75" width="5.02" height="5.21"/>
<rect x="65.61" y="6.29" width="5.02" height="5.21"/>
<rect x="60.55" y="142.31" width="5.06" height="5.21"/>
<rect x="60.55" y="131.82" width="5.06" height="5.25"/>
<rect x="60.55" y="121.37" width="5.06" height="5.25"/>
<rect x="60.55" y="27.2" width="5.06" height="5.25"/>
<rect x="60.55" y="16.75" width="5.06" height="5.21"/>
<rect x="60.55" y="6.29" width="5.06" height="5.21"/>
<rect x="55.53" y="142.31" width="5.02" height="5.21"/>
<rect x="55.53" y="131.82" width="5.02" height="5.25"/>
<rect x="55.53" y="121.37" width="5.02" height="5.25"/>
<rect x="55.53" y="27.2" width="5.02" height="5.25"/>
<rect x="55.53" y="16.75" width="5.02" height="5.21"/>
<rect x="55.53" y="6.29" width="5.02" height="5.21"/>
<rect x="50.5" y="142.31" width="5.02" height="5.21"/>
<rect x="50.5" y="131.82" width="5.02" height="5.25"/>
<rect x="50.5" y="121.37" width="5.02" height="5.25"/>
<rect x="50.5" y="27.2" width="5.02" height="5.25"/>
<rect x="50.5" y="16.75" width="5.02" height="5.21"/>
<rect x="50.5" y="6.29" width="5.02" height="5.21"/>
<rect x="45.48" y="142.31" width="5.02" height="5.21"/>
<rect x="45.48" y="131.82" width="5.02" height="5.25"/>
<rect x="45.48" y="121.37" width="5.02" height="5.25"/>
<rect x="45.48" y="27.2" width="5.02" height="5.25"/>
<rect x="45.48" y="16.75" width="5.02" height="5.21"/>
<rect x="45.48" y="6.29" width="5.02" height="5.21"/>
<rect x="40.45" y="142.31" width="5.02" height="5.21"/>
<rect x="40.45" y="131.82" width="5.02" height="5.25"/>
<rect x="40.45" y="121.37" width="5.02" height="5.25"/>
<rect x="40.45" y="27.2" width="5.02" height="5.25"/>
<rect x="40.45" y="16.75" width="5.02" height="5.21"/>
<rect x="40.45" y="6.29" width="5.02" height="5.21"/>
<rect x="35.43" y="142.31" width="5.02" height="5.21"/>
<rect x="35.43" y="131.82" width="5.02" height="5.25"/>
<rect x="35.43" y="121.37" width="5.02" height="5.25"/>
<rect x="35.43" y="110.91" width="5.02" height="5.25"/>
<rect x="35.43" y="100.46" width="5.02" height="5.21"/>
<rect x="35.43" y="89.97" width="5.02" height="5.25"/>
<rect x="35.43" y="79.51" width="5.02" height="5.25"/>
<rect x="35.43" y="69.06" width="5.02" height="5.25"/>
<rect x="35.43" y="58.6" width="5.02" height="5.21"/>
<rect x="35.43" y="48.15" width="5.02" height="5.21"/>
<rect x="35.43" y="37.66" width="5.02" height="5.25"/>
<rect x="35.43" y="27.2" width="5.02" height="5.25"/>
<rect x="35.43" y="16.75" width="5.02" height="5.21"/>
<rect x="35.43" y="6.29" width="5.02" height="5.21"/>
<rect x="30.41" y="142.31" width="5.02" height="5.21"/>
<rect x="30.41" y="131.82" width="5.02" height="5.25"/>
<rect x="30.41" y="121.37" width="5.02" height="5.25"/>
<rect x="30.41" y="110.91" width="5.02" height="5.25"/>
<rect x="30.41" y="100.46" width="5.02" height="5.21"/>
<rect x="30.41" y="89.97" width="5.02" height="5.25"/>
<rect x="30.41" y="79.51" width="5.02" height="5.25"/>
<rect x="30.41" y="69.06" width="5.02" height="5.25"/>
<rect x="30.41" y="58.6" width="5.02" height="5.21"/>
<rect x="30.41" y="48.15" width="5.02" height="5.21"/>
<rect x="30.41" y="37.66" width="5.02" height="5.25"/>
<rect x="30.41" y="27.2" width="5.02" height="5.25"/>
<rect x="30.41" y="16.75" width="5.02" height="5.21"/>
<rect x="30.41" y="6.29" width="5.02" height="5.21"/>
<rect x="25.38" y="142.31" width="5.02" height="5.21"/>
<rect x="25.38" y="131.82" width="5.02" height="5.25"/>
<rect x="25.38" y="121.37" width="5.02" height="5.25"/>
<rect x="25.38" y="110.91" width="5.02" height="5.25"/>
<rect x="25.38" y="100.46" width="5.02" height="5.21"/>
<rect x="25.38" y="89.97" width="5.02" height="5.25"/>
<rect x="25.38" y="79.51" width="5.02" height="5.25"/>
<rect x="25.38" y="69.06" width="5.02" height="5.25"/>
<rect x="25.38" y="58.6" width="5.02" height="5.21"/>
<rect x="25.38" y="48.15" width="5.02" height="5.21"/>
<rect x="25.38" y="37.66" width="5.02" height="5.25"/>
<rect x="25.38" y="27.2" width="5.02" height="5.25"/>
<rect x="25.38" y="16.75" width="5.02" height="5.21"/>
<rect x="25.38" y="6.29" width="5.02" height="5.21"/>
<rect x="20.36" y="142.31" width="5.02" height="5.21"/>
<rect x="20.36" y="131.82" width="5.02" height="5.25"/>
<rect x="20.36" y="121.37" width="5.02" height="5.25"/>
<rect x="20.36" y="110.91" width="5.02" height="5.25"/>
<rect x="20.36" y="100.46" width="5.02" height="5.21"/>
<rect x="20.36" y="89.97" width="5.02" height="5.25"/>
<rect x="20.36" y="79.51" width="5.02" height="5.25"/>
<rect x="20.36" y="69.06" width="5.02" height="5.25"/>
<rect x="20.36" y="58.6" width="5.02" height="5.21"/>
<rect x="20.36" y="48.15" width="5.02" height="5.21"/>
<rect x="20.36" y="37.66" width="5.02" height="5.25"/>
<rect x="20.36" y="27.2" width="5.02" height="5.25"/>
<rect x="20.36" y="16.75" width="5.02" height="5.21"/>
<rect x="20.36" y="6.29" width="5.02" height="5.21"/>
<rect x="15.33" y="142.31" width="5.02" height="5.21"/>
<rect x="15.33" y="131.82" width="5.02" height="5.25"/>
<rect x="15.33" y="121.37" width="5.02" height="5.25"/>
<rect x="15.33" y="110.91" width="5.02" height="5.25"/>
<rect x="15.33" y="100.46" width="5.02" height="5.21"/>
<rect x="15.33" y="89.97" width="5.02" height="5.25"/>
<rect x="15.33" y="79.51" width="5.02" height="5.25"/>
<rect x="15.33" y="69.06" width="5.02" height="5.25"/>
<rect x="15.33" y="58.6" width="5.02" height="5.21"/>
<rect x="15.33" y="48.15" width="5.02" height="5.21"/>
<rect x="15.33" y="37.66" width="5.02" height="5.25"/>
<rect x="15.33" y="27.2" width="5.02" height="5.25"/>
<rect x="15.33" y="16.75" width="5.02" height="5.21"/>
<rect x="15.33" y="6.29" width="5.02" height="5.21"/>
<rect x="10.31" y="142.31" width="5.02" height="5.21"/>
<rect x="10.31" y="131.82" width="5.02" height="5.25"/>
<rect x="10.31" y="121.37" width="5.02" height="5.25"/>
<rect x="10.31" y="110.91" width="5.02" height="5.25"/>
<rect x="10.31" y="100.46" width="5.02" height="5.21"/>
<rect x="10.31" y="89.97" width="5.02" height="5.25"/>
<rect x="10.31" y="79.51" width="5.02" height="5.25"/>
<rect x="10.31" y="69.06" width="5.02" height="5.25"/>
<rect x="10.31" y="58.6" width="5.02" height="5.21"/>
<rect x="10.31" y="48.15" width="5.02" height="5.21"/>
<rect x="10.31" y="37.66" width="5.02" height="5.25"/>
<rect x="10.31" y="27.2" width="5.02" height="5.25"/>
<rect x="10.31" y="16.75" width="5.02" height="5.21"/>
<rect x="10.31" y="6.29" width="5.02" height="5.21"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -0,0 +1,976 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.9 65.08">
<defs>
<style>
.cls-1 {
fill: #fedf02;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<g>
<rect class="cls-2" x="46.1" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="46.1" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="46.1" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="46.1" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="46.1" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="46.1" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="46.1" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="46.1" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="46.1" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="46.1" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="46.1" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="46.1" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="43.8" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="43.8" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="43.8" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="43.8" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="43.8" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="43.8" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="43.8" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="43.8" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="43.8" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="43.8" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="43.8" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="43.8" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="43.8" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="43.8" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="41.49" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="41.49" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="41.49" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="41.49" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="41.49" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="41.49" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="41.49" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="41.49" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="41.49" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="41.49" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="41.49" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="41.49" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="41.49" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="41.49" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="39.19" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="39.19" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="39.19" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="39.19" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="39.19" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="39.19" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="39.19" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="39.19" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="39.19" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="39.19" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="39.19" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="39.19" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="39.19" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="39.19" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="36.88" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="36.88" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="36.88" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="36.88" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="36.88" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="36.88" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="36.88" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="36.88" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="36.88" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="36.88" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="36.88" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="36.88" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="36.88" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="36.88" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="34.58" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="34.58" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="34.58" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="34.58" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="34.58" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="34.58" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="34.58" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="34.58" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="34.58" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="34.58" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="34.58" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="34.58" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="34.58" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="34.58" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="32.28" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="32.28" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="32.28" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="32.28" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="32.28" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="32.28" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="32.28" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="32.28" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="29.97" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="29.97" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="29.97" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="29.97" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="29.97" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="29.97" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="29.97" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="29.97" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="27.67" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="27.67" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="27.67" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="27.67" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="27.67" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="27.67" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="27.67" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="27.67" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="25.35" y="62.4" width="2.32" height="2.39"/>
<rect class="cls-2" x="25.35" y="57.58" width="2.32" height="2.41"/>
<rect class="cls-2" x="25.35" y="52.79" width="2.32" height="2.41"/>
<rect class="cls-2" x="25.35" y="33.59" width="2.32" height="2.41"/>
<rect class="cls-2" x="25.35" y="28.8" width="2.32" height="2.41"/>
<rect class="cls-2" x="25.35" y="9.6" width="2.32" height="2.41"/>
<rect class="cls-2" x="25.35" y="4.81" width="2.32" height="2.41"/>
<rect class="cls-2" x="25.35" y=".01" width="2.32" height="2.39"/>
<rect class="cls-2" x="23.04" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="23.04" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="23.04" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="23.04" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="23.04" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="23.04" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="23.04" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="23.04" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="20.74" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="20.74" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="20.74" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="20.74" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="20.74" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="20.74" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="20.74" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="20.74" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="18.43" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="18.43" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="18.43" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="18.43" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="18.43" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="18.43" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="18.43" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="18.43" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="16.13" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="16.13" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="16.13" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="16.13" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="16.13" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="16.13" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="13.83" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="13.83" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="13.83" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="13.83" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="13.83" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="13.83" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="11.52" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="11.52" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="11.52" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="11.52" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="11.52" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="11.52" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="9.22" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="9.22" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="9.22" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="9.22" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="9.22" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="9.22" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="6.91" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="6.91" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="6.91" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="6.91" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="6.91" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="6.91" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="4.61" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="4.61" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="4.61" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="4.61" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="4.61" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="4.61" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="2.31" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="2.31" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="2.31" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="2.31" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="2.31" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="2.31" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" y="4.81" width="2.3" height="2.41"/>
</g>
<g>
<rect class="cls-2" x="110.7" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="110.7" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="110.7" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="110.7" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="110.7" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="110.7" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="110.7" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="110.7" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="110.7" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="110.7" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="108.39" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="108.39" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="108.39" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="108.39" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="108.39" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="108.39" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="108.39" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="108.39" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="108.39" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="108.39" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="108.39" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="108.39" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="106.09" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="106.09" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="106.09" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="106.09" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="106.09" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="106.09" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="106.09" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="106.09" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="106.09" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="106.09" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="106.09" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="106.09" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="103.79" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="103.79" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="103.79" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="103.79" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="103.79" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="103.79" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="103.79" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="103.79" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="103.79" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="103.79" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="103.79" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="103.79" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="103.79" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="103.79" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="101.48" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="101.48" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="101.48" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="101.48" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="101.48" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="101.48" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="101.48" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="101.48" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="101.48" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="101.48" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="101.48" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="101.48" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="101.48" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="101.48" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="99.16" y="62.4" width="2.32" height="2.39"/>
<rect class="cls-2" x="99.16" y="57.58" width="2.32" height="2.41"/>
<rect class="cls-2" x="99.16" y="52.79" width="2.32" height="2.41"/>
<rect class="cls-2" x="99.16" y="47.99" width="2.32" height="2.41"/>
<rect class="cls-2" x="99.16" y="43.2" width="2.32" height="2.39"/>
<rect class="cls-2" x="99.16" y="38.4" width="2.32" height="2.39"/>
<rect class="cls-2" x="99.16" y="33.59" width="2.32" height="2.41"/>
<rect class="cls-2" x="99.16" y="28.8" width="2.32" height="2.41"/>
<rect class="cls-2" x="99.16" y="24" width="2.32" height="2.39"/>
<rect class="cls-2" x="99.16" y="19.21" width="2.32" height="2.39"/>
<rect class="cls-2" x="99.16" y="14.4" width="2.32" height="2.41"/>
<rect class="cls-2" x="99.16" y="9.6" width="2.32" height="2.41"/>
<rect class="cls-2" x="99.16" y="4.81" width="2.32" height="2.41"/>
<rect class="cls-2" x="99.16" y=".01" width="2.32" height="2.39"/>
<rect class="cls-2" x="96.86" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="96.86" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="96.86" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="96.86" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="96.86" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="96.86" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="96.86" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="96.86" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="94.55" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="94.55" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="94.55" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="94.55" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="94.55" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="94.55" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="92.25" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="92.25" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="92.25" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="92.25" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="92.25" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="92.25" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="89.94" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="89.94" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="89.94" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="89.94" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="89.94" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="89.94" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="87.64" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="87.64" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="87.64" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="87.64" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="87.64" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="87.64" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="85.34" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="85.34" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="85.34" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="85.34" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="85.34" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="85.34" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="83.03" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="83.03" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="83.03" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="83.03" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="83.03" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="83.03" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="80.73" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="80.73" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="80.73" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="80.73" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="80.73" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="80.73" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="78.42" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="78.42" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="78.42" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="78.42" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="78.42" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="78.42" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="76.12" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="76.12" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="76.12" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="76.12" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="76.12" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="76.12" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="76.12" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="76.12" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="76.12" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="76.12" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="76.12" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="76.12" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="76.12" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="76.12" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="73.81" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="73.81" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="73.81" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="73.81" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="73.81" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="73.81" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="73.81" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="73.81" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="73.81" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="73.81" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="73.81" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="73.81" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="73.81" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="73.81" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="71.51" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="71.51" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="71.51" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="71.51" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="71.51" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="71.51" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="71.51" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="71.51" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="71.51" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="71.51" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="71.51" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="71.51" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="71.51" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="71.51" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="69.21" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="69.21" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="69.21" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="69.21" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="69.21" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="69.21" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="69.21" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="69.21" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="69.21" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="69.21" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="69.21" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="69.21" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="69.21" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="69.21" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="66.9" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="66.9" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="66.9" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="66.9" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="66.9" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="66.9" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="66.9" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="66.9" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="66.9" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="66.9" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="66.9" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="66.9" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="66.9" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="66.9" y=".01" width="2.3" height="2.39"/>
<rect class="cls-2" x="64.6" y="62.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="64.6" y="57.58" width="2.3" height="2.41"/>
<rect class="cls-2" x="64.6" y="52.79" width="2.3" height="2.41"/>
<rect class="cls-2" x="64.6" y="47.99" width="2.3" height="2.41"/>
<rect class="cls-2" x="64.6" y="43.2" width="2.3" height="2.39"/>
<rect class="cls-2" x="64.6" y="38.4" width="2.3" height="2.39"/>
<rect class="cls-2" x="64.6" y="33.59" width="2.3" height="2.41"/>
<rect class="cls-2" x="64.6" y="28.8" width="2.3" height="2.41"/>
<rect class="cls-2" x="64.6" y="24" width="2.3" height="2.39"/>
<rect class="cls-2" x="64.6" y="19.21" width="2.3" height="2.39"/>
<rect class="cls-2" x="64.6" y="14.4" width="2.3" height="2.41"/>
<rect class="cls-2" x="64.6" y="9.6" width="2.3" height="2.41"/>
<rect class="cls-2" x="64.6" y="4.81" width="2.3" height="2.41"/>
<rect class="cls-2" x="64.6" y=".01" width="2.3" height="2.39"/>
</g>
<g>
<rect class="cls-1" x="184.85" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="184.85" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="184.85" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="182.55" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="182.55" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="182.55" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="180.24" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="180.24" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="180.24" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="177.94" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="177.94" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="177.94" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="175.63" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="175.63" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="175.63" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="173.33" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="173.33" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="173.33" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="171.03" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="171.03" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="171.03" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="168.72" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="168.72" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="168.72" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="168.72" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="168.72" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="166.42" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="166.42" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="166.42" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="166.42" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="166.42" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="164.11" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="164.11" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="164.11" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="164.11" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="164.11" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="161.81" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="161.81" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="161.81" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="161.81" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="161.81" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="159.5" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="159.5" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="159.5" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="159.5" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="159.5" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="157.2" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="157.2" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="157.2" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="157.2" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="157.2" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="154.88" y="28.83" width="2.32" height="2.41"/>
<rect class="cls-1" x="154.88" y="24.04" width="2.32" height="2.41"/>
<rect class="cls-1" x="154.88" y="9.65" width="2.32" height="2.39"/>
<rect class="cls-1" x="154.88" y="4.84" width="2.32" height="2.41"/>
<rect class="cls-1" x="154.88" y=".05" width="2.32" height="2.41"/>
<rect class="cls-1" x="152.58" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="152.58" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="152.58" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="152.58" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="152.58" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="150.27" y="62.43" width="2.3" height="2.41"/>
<rect class="cls-1" x="150.27" y="57.63" width="2.3" height="2.39"/>
<rect class="cls-1" x="150.27" y="52.84" width="2.3" height="2.39"/>
<rect class="cls-1" x="150.27" y="48.03" width="2.3" height="2.41"/>
<rect class="cls-1" x="150.27" y="43.23" width="2.3" height="2.41"/>
<rect class="cls-1" x="150.27" y="38.44" width="2.3" height="2.41"/>
<rect class="cls-1" x="150.27" y="33.64" width="2.3" height="2.39"/>
<rect class="cls-1" x="150.27" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="150.27" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="150.27" y="19.24" width="2.3" height="2.41"/>
<rect class="cls-1" x="150.27" y="14.45" width="2.3" height="2.39"/>
<rect class="cls-1" x="150.27" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="150.27" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="150.27" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="147.97" y="62.43" width="2.3" height="2.41"/>
<rect class="cls-1" x="147.97" y="57.63" width="2.3" height="2.39"/>
<rect class="cls-1" x="147.97" y="52.84" width="2.3" height="2.39"/>
<rect class="cls-1" x="147.97" y="48.03" width="2.3" height="2.41"/>
<rect class="cls-1" x="147.97" y="43.23" width="2.3" height="2.41"/>
<rect class="cls-1" x="147.97" y="38.44" width="2.3" height="2.41"/>
<rect class="cls-1" x="147.97" y="33.64" width="2.3" height="2.39"/>
<rect class="cls-1" x="147.97" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="147.97" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="147.97" y="19.24" width="2.3" height="2.41"/>
<rect class="cls-1" x="147.97" y="14.45" width="2.3" height="2.39"/>
<rect class="cls-1" x="147.97" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="147.97" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="147.97" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="145.66" y="62.43" width="2.3" height="2.41"/>
<rect class="cls-1" x="145.66" y="57.63" width="2.3" height="2.39"/>
<rect class="cls-1" x="145.66" y="52.84" width="2.3" height="2.39"/>
<rect class="cls-1" x="145.66" y="48.03" width="2.3" height="2.41"/>
<rect class="cls-1" x="145.66" y="43.23" width="2.3" height="2.41"/>
<rect class="cls-1" x="145.66" y="38.44" width="2.3" height="2.41"/>
<rect class="cls-1" x="145.66" y="33.64" width="2.3" height="2.39"/>
<rect class="cls-1" x="145.66" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="145.66" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="145.66" y="19.24" width="2.3" height="2.41"/>
<rect class="cls-1" x="145.66" y="14.45" width="2.3" height="2.39"/>
<rect class="cls-1" x="145.66" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="145.66" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="145.66" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="143.36" y="62.43" width="2.3" height="2.41"/>
<rect class="cls-1" x="143.36" y="57.63" width="2.3" height="2.39"/>
<rect class="cls-1" x="143.36" y="52.84" width="2.3" height="2.39"/>
<rect class="cls-1" x="143.36" y="48.03" width="2.3" height="2.41"/>
<rect class="cls-1" x="143.36" y="43.23" width="2.3" height="2.41"/>
<rect class="cls-1" x="143.36" y="38.44" width="2.3" height="2.41"/>
<rect class="cls-1" x="143.36" y="33.64" width="2.3" height="2.39"/>
<rect class="cls-1" x="143.36" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="143.36" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="143.36" y="19.24" width="2.3" height="2.41"/>
<rect class="cls-1" x="143.36" y="14.45" width="2.3" height="2.39"/>
<rect class="cls-1" x="143.36" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="143.36" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="143.36" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="141.05" y="62.43" width="2.3" height="2.41"/>
<rect class="cls-1" x="141.05" y="57.63" width="2.3" height="2.39"/>
<rect class="cls-1" x="141.05" y="52.84" width="2.3" height="2.39"/>
<rect class="cls-1" x="141.05" y="48.03" width="2.3" height="2.41"/>
<rect class="cls-1" x="141.05" y="43.23" width="2.3" height="2.41"/>
<rect class="cls-1" x="141.05" y="38.44" width="2.3" height="2.41"/>
<rect class="cls-1" x="141.05" y="33.64" width="2.3" height="2.39"/>
<rect class="cls-1" x="141.05" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="141.05" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="141.05" y="19.24" width="2.3" height="2.41"/>
<rect class="cls-1" x="141.05" y="14.45" width="2.3" height="2.39"/>
<rect class="cls-1" x="141.05" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="141.05" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="141.05" y=".05" width="2.3" height="2.41"/>
<rect class="cls-1" x="138.75" y="62.43" width="2.3" height="2.41"/>
<rect class="cls-1" x="138.75" y="57.63" width="2.3" height="2.39"/>
<rect class="cls-1" x="138.75" y="52.84" width="2.3" height="2.39"/>
<rect class="cls-1" x="138.75" y="48.03" width="2.3" height="2.41"/>
<rect class="cls-1" x="138.75" y="43.23" width="2.3" height="2.41"/>
<rect class="cls-1" x="138.75" y="38.44" width="2.3" height="2.41"/>
<rect class="cls-1" x="138.75" y="33.64" width="2.3" height="2.39"/>
<rect class="cls-1" x="138.75" y="28.83" width="2.3" height="2.41"/>
<rect class="cls-1" x="138.75" y="24.04" width="2.3" height="2.41"/>
<rect class="cls-1" x="138.75" y="19.24" width="2.3" height="2.41"/>
<rect class="cls-1" x="138.75" y="14.45" width="2.3" height="2.39"/>
<rect class="cls-1" x="138.75" y="9.65" width="2.3" height="2.39"/>
<rect class="cls-1" x="138.75" y="4.84" width="2.3" height="2.41"/>
<rect class="cls-1" x="138.75" y=".05" width="2.3" height="2.41"/>
</g>
<g>
<rect class="cls-1" x="316.59" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="316.59" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="316.59" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="314.29" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="314.29" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="314.29" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="314.29" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="314.29" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="314.29" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="314.29" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="314.29" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="311.99" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="311.99" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="311.99" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="311.99" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="311.99" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="311.99" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="311.99" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="311.99" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="311.99" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="311.99" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="309.68" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="309.68" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="309.68" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="309.68" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="309.68" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="309.68" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="309.68" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="309.68" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="309.68" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="309.68" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="309.68" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="309.68" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="307.38" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="307.38" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="307.38" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="307.38" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="307.38" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="307.38" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="307.38" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="307.38" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="307.38" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="307.38" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="307.38" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="307.38" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="307.38" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="307.38" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="305.07" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="305.07" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="305.07" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="305.07" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="305.07" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="305.07" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="305.07" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="305.07" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="305.07" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="305.07" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="305.07" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="305.07" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="305.07" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="305.07" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="302.77" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="302.77" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="302.77" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="302.77" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="302.77" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="302.77" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="302.77" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="302.77" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="302.77" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="302.77" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="302.77" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="302.77" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="300.46" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="300.46" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="300.46" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="300.46" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="300.46" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="300.46" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="300.46" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="300.46" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="298.16" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="298.16" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="298.16" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="298.16" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="298.16" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="298.16" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="295.86" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="295.86" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="295.86" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="295.86" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="295.86" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="295.86" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="293.55" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="293.55" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="293.55" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="293.55" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="293.55" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="293.55" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="291.25" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="291.25" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="291.25" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="291.25" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="291.25" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="291.25" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="288.94" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="288.94" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="288.94" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="288.94" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="288.94" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="288.94" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="286.62" y="62.68" width="2.32" height="2.41"/>
<rect class="cls-1" x="286.62" y="57.88" width="2.32" height="2.39"/>
<rect class="cls-1" x="286.62" y="33.89" width="2.32" height="2.39"/>
<rect class="cls-1" x="286.62" y="29.08" width="2.32" height="2.41"/>
<rect class="cls-1" x="286.62" y="5.09" width="2.32" height="2.41"/>
<rect class="cls-1" x="286.62" y=".29" width="2.32" height="2.41"/>
<rect class="cls-1" x="284.32" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="284.32" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="284.32" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="284.32" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="284.32" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="284.32" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="282.01" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="282.01" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="282.01" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="282.01" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="282.01" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="282.01" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="282.01" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="282.01" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="282.01" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="282.01" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="282.01" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="282.01" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="282.01" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="282.01" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="279.71" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="279.71" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="279.71" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="279.71" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="279.71" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="279.71" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="279.71" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="279.71" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="279.71" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="279.71" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="279.71" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="279.71" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="279.71" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="279.71" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="277.41" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="277.41" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="277.41" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="277.41" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="277.41" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="277.41" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="277.41" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="277.41" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="277.41" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="277.41" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="277.41" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="277.41" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="277.41" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="277.41" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="275.1" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="275.1" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="275.1" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="275.1" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="275.1" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="275.1" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="275.1" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="275.1" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="275.1" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="275.1" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="275.1" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="275.1" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="275.1" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="275.1" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="272.8" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="272.8" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="272.8" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="272.8" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="272.8" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="272.8" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="272.8" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="272.8" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="272.8" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="272.8" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="272.8" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="272.8" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="272.8" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="272.8" y=".29" width="2.3" height="2.41"/>
<rect class="cls-1" x="270.49" y="62.68" width="2.3" height="2.41"/>
<rect class="cls-1" x="270.49" y="57.88" width="2.3" height="2.39"/>
<rect class="cls-1" x="270.49" y="53.09" width="2.3" height="2.39"/>
<rect class="cls-1" x="270.49" y="48.27" width="2.3" height="2.41"/>
<rect class="cls-1" x="270.49" y="43.48" width="2.3" height="2.41"/>
<rect class="cls-1" x="270.49" y="38.68" width="2.3" height="2.39"/>
<rect class="cls-1" x="270.49" y="33.89" width="2.3" height="2.39"/>
<rect class="cls-1" x="270.49" y="29.08" width="2.3" height="2.41"/>
<rect class="cls-1" x="270.49" y="24.28" width="2.3" height="2.41"/>
<rect class="cls-1" x="270.49" y="19.49" width="2.3" height="2.41"/>
<rect class="cls-1" x="270.49" y="14.69" width="2.3" height="2.39"/>
<rect class="cls-1" x="270.49" y="9.88" width="2.3" height="2.41"/>
<rect class="cls-1" x="270.49" y="5.09" width="2.3" height="2.41"/>
<rect class="cls-1" x="270.49" y=".29" width="2.3" height="2.41"/>
</g>
<g>
<rect class="cls-1" x="250.75" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="249.2" y="62.58" width="1.54" height="2.41"/>
<rect class="cls-1" x="249.2" y="57.75" width="1.54" height="2.41"/>
<rect class="cls-1" x="247.65" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="247.65" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="247.65" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="246.09" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="246.09" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="246.09" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="246.09" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="244.53" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="244.53" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="244.53" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="244.53" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="244.53" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="242.97" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="242.97" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="242.97" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="242.97" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="242.97" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="242.97" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="241.41" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="241.41" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="241.41" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="241.41" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="241.41" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="241.41" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="241.41" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="239.85" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="239.85" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="239.85" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="239.85" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="239.85" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="239.85" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="239.85" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="238.29" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="238.29" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="238.29" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="238.29" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="238.29" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="238.29" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="238.29" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="236.74" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="236.74" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="236.74" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="236.74" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="236.74" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="236.74" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="236.74" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="235.18" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="235.18" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="235.18" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="235.18" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="235.18" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="235.18" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="235.18" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="233.62" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="233.62" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="233.62" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="233.62" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="233.62" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="233.62" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="233.62" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="233.62" y="9.62" width="1.56" height="2.41"/>
<rect class="cls-1" x="232.06" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="232.06" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="232.06" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="232.06" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="232.06" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="232.06" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="232.06" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="232.06" y="9.62" width="1.56" height="2.41"/>
<rect class="cls-1" x="232.06" y="4.81" width="1.56" height="2.41"/>
<rect class="cls-1" x="230.5" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="230.5" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="230.5" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="230.5" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="230.5" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="230.5" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="230.5" y="9.62" width="1.56" height="2.41"/>
<rect class="cls-1" x="230.5" y="4.81" width="1.56" height="2.41"/>
<rect class="cls-1" x="230.5" width="1.56" height="2.41"/>
<rect class="cls-1" x="228.94" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="228.94" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="228.94" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="228.94" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="228.94" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="228.94" y="9.62" width="1.56" height="2.41"/>
<rect class="cls-1" x="228.94" y="4.81" width="1.56" height="2.41"/>
<rect class="cls-1" x="228.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="227.38" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="227.38" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="227.38" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="227.38" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="227.38" y="9.62" width="1.56" height="2.41"/>
<rect class="cls-1" x="227.38" y="4.81" width="1.56" height="2.41"/>
<rect class="cls-1" x="227.38" width="1.56" height="2.41"/>
<rect class="cls-1" x="225.84" y="43.32" width="1.54" height="2.41"/>
<rect class="cls-1" x="225.84" y="38.51" width="1.54" height="2.41"/>
<rect class="cls-1" x="225.84" y="14.43" width="1.54" height="2.41"/>
<rect class="cls-1" x="225.84" y="9.62" width="1.54" height="2.41"/>
<rect class="cls-1" x="225.84" y="4.81" width="1.54" height="2.41"/>
<rect class="cls-1" x="225.84" width="1.54" height="2.41"/>
<rect class="cls-1" x="224.28" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="224.28" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="224.28" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="224.28" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="224.28" y="9.62" width="1.56" height="2.41"/>
<rect class="cls-1" x="224.28" y="4.81" width="1.56" height="2.41"/>
<rect class="cls-1" x="224.28" width="1.56" height="2.41"/>
<rect class="cls-1" x="222.72" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="222.72" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="222.72" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="222.72" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="222.72" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="222.72" y="9.62" width="1.56" height="2.41"/>
<rect class="cls-1" x="222.72" y="4.81" width="1.56" height="2.41"/>
<rect class="cls-1" x="222.72" width="1.56" height="2.41"/>
<rect class="cls-1" x="221.16" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="221.16" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="221.16" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="221.16" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="221.16" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="221.16" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="221.16" y="9.62" width="1.56" height="2.41"/>
<rect class="cls-1" x="221.16" y="4.81" width="1.56" height="2.41"/>
<rect class="cls-1" x="221.16" width="1.56" height="2.41"/>
<rect class="cls-1" x="219.61" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="219.61" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="219.61" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="219.61" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="219.61" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="219.61" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="219.61" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="219.61" y="9.62" width="1.56" height="2.41"/>
<rect class="cls-1" x="219.61" y="4.81" width="1.56" height="2.41"/>
<rect class="cls-1" x="218.05" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="218.05" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="218.05" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="218.05" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="218.05" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="218.05" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="218.05" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="218.05" y="9.62" width="1.56" height="2.41"/>
<rect class="cls-1" x="216.49" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="216.49" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="216.49" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="216.49" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="216.49" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="216.49" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="216.49" y="14.43" width="1.56" height="2.41"/>
<rect class="cls-1" x="214.93" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="214.93" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="214.93" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="214.93" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="214.93" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="214.93" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="214.93" y="19.24" width="1.56" height="2.41"/>
<rect class="cls-1" x="213.37" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="213.37" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="213.37" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="213.37" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="213.37" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="213.37" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="213.37" y="24.05" width="1.56" height="2.41"/>
<rect class="cls-1" x="211.81" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="211.81" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="211.81" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="211.81" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="211.81" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="211.81" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="211.81" y="28.88" width="1.56" height="2.41"/>
<rect class="cls-1" x="210.25" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="210.25" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="210.25" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="210.25" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="210.25" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="210.25" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="210.25" y="33.69" width="1.56" height="2.41"/>
<rect class="cls-1" x="208.7" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="208.7" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="208.7" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="208.7" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="208.7" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="208.7" y="38.51" width="1.56" height="2.41"/>
<rect class="cls-1" x="207.14" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="207.14" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="207.14" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="207.14" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="207.14" y="43.32" width="1.56" height="2.41"/>
<rect class="cls-1" x="205.58" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="205.58" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="205.58" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="205.58" y="48.13" width="1.56" height="2.41"/>
<rect class="cls-1" x="204.02" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="204.02" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="204.02" y="52.94" width="1.56" height="2.41"/>
<rect class="cls-1" x="202.46" y="62.58" width="1.56" height="2.41"/>
<rect class="cls-1" x="202.46" y="57.75" width="1.56" height="2.41"/>
<rect class="cls-1" x="200.92" y="62.58" width="1.54" height="2.41"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 69 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

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