unifi-network-maps 1.4.15__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. unifi_network_maps/__init__.py +1 -1
  2. unifi_network_maps/adapters/unifi.py +80 -96
  3. unifi_network_maps/assets/icons/modern/ap.svg +9 -0
  4. unifi_network_maps/assets/icons/modern/camera.svg +9 -0
  5. unifi_network_maps/assets/icons/modern/client.svg +9 -0
  6. unifi_network_maps/assets/icons/modern/game_console.svg +10 -0
  7. unifi_network_maps/assets/icons/modern/gateway.svg +17 -0
  8. unifi_network_maps/assets/icons/modern/iot.svg +9 -0
  9. unifi_network_maps/assets/icons/modern/nas.svg +9 -0
  10. unifi_network_maps/assets/icons/modern/other.svg +10 -0
  11. unifi_network_maps/assets/icons/modern/phone.svg +10 -0
  12. unifi_network_maps/assets/icons/modern/printer.svg +9 -0
  13. unifi_network_maps/assets/icons/modern/speaker.svg +10 -0
  14. unifi_network_maps/assets/icons/modern/switch.svg +10 -0
  15. unifi_network_maps/assets/icons/modern/tv.svg +10 -0
  16. unifi_network_maps/assets/icons/modern-flat/ap.svg +5 -0
  17. unifi_network_maps/assets/icons/modern-flat/camera.svg +5 -0
  18. unifi_network_maps/assets/icons/modern-flat/client.svg +5 -0
  19. unifi_network_maps/assets/icons/modern-flat/game_console.svg +6 -0
  20. unifi_network_maps/assets/icons/modern-flat/gateway.svg +13 -0
  21. unifi_network_maps/assets/icons/modern-flat/iot.svg +5 -0
  22. unifi_network_maps/assets/icons/modern-flat/nas.svg +5 -0
  23. unifi_network_maps/assets/icons/modern-flat/other.svg +6 -0
  24. unifi_network_maps/assets/icons/modern-flat/phone.svg +6 -0
  25. unifi_network_maps/assets/icons/modern-flat/printer.svg +5 -0
  26. unifi_network_maps/assets/icons/modern-flat/speaker.svg +6 -0
  27. unifi_network_maps/assets/icons/modern-flat/switch.svg +6 -0
  28. unifi_network_maps/assets/icons/modern-flat/tv.svg +6 -0
  29. unifi_network_maps/assets/themes/dark.yaml +53 -10
  30. unifi_network_maps/assets/themes/default.yaml +34 -0
  31. unifi_network_maps/assets/themes/minimal-dark.yaml +98 -0
  32. unifi_network_maps/assets/themes/minimal.yaml +92 -0
  33. unifi_network_maps/assets/themes/unifi-dark.yaml +97 -0
  34. unifi_network_maps/assets/themes/unifi.yaml +92 -0
  35. unifi_network_maps/cli/args.py +54 -0
  36. unifi_network_maps/cli/main.py +18 -7
  37. unifi_network_maps/cli/render.py +79 -27
  38. unifi_network_maps/cli/runtime.py +29 -15
  39. unifi_network_maps/io/debug.py +2 -1
  40. unifi_network_maps/io/export.py +19 -13
  41. unifi_network_maps/io/mock_data.py +5 -3
  42. unifi_network_maps/io/paths.py +5 -3
  43. unifi_network_maps/model/classify.py +199 -0
  44. unifi_network_maps/model/clients.py +271 -0
  45. unifi_network_maps/model/connection.py +37 -0
  46. unifi_network_maps/model/diff.py +544 -0
  47. unifi_network_maps/model/edges.py +558 -0
  48. unifi_network_maps/model/helpers.py +64 -0
  49. unifi_network_maps/model/lldp.py +20 -25
  50. unifi_network_maps/model/mock.py +110 -23
  51. unifi_network_maps/model/snapshot.py +294 -0
  52. unifi_network_maps/model/topology.py +143 -951
  53. unifi_network_maps/model/topology_coerce.py +339 -0
  54. unifi_network_maps/model/vlans.py +32 -46
  55. unifi_network_maps/model/wan.py +132 -0
  56. unifi_network_maps/render/device_ports_md.py +39 -97
  57. unifi_network_maps/render/device_summary.py +53 -0
  58. unifi_network_maps/render/lldp_md.py +29 -219
  59. unifi_network_maps/render/markdown_tables.py +8 -0
  60. unifi_network_maps/render/mermaid.py +11 -2
  61. unifi_network_maps/render/mkdocs.py +2 -1
  62. unifi_network_maps/render/svg.py +566 -908
  63. unifi_network_maps/render/svg_icons.py +231 -0
  64. unifi_network_maps/render/svg_isometric.py +1196 -0
  65. unifi_network_maps/render/svg_labels.py +184 -0
  66. unifi_network_maps/render/svg_theme.py +166 -32
  67. unifi_network_maps/render/theme.py +86 -1
  68. {unifi_network_maps-1.4.15.dist-info → unifi_network_maps-1.5.0.dist-info}/METADATA +107 -31
  69. {unifi_network_maps-1.4.15.dist-info → unifi_network_maps-1.5.0.dist-info}/RECORD +73 -31
  70. unifi_network_maps/assets/icons/isometric/printer.svg +0 -122
  71. {unifi_network_maps-1.4.15.dist-info → unifi_network_maps-1.5.0.dist-info}/WHEEL +0 -0
  72. {unifi_network_maps-1.4.15.dist-info → unifi_network_maps-1.5.0.dist-info}/entry_points.txt +0 -0
  73. {unifi_network_maps-1.4.15.dist-info → unifi_network_maps-1.5.0.dist-info}/licenses/LICENSE +0 -0
  74. {unifi_network_maps-1.4.15.dist-info → unifi_network_maps-1.5.0.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- __version__ = "1.4.15"
1
+ __version__ = "1.5.0"
@@ -17,6 +17,7 @@ from pathlib import Path
17
17
  from typing import IO, TYPE_CHECKING
18
18
 
19
19
  from ..io.paths import resolve_cache_dir
20
+ from ..model.helpers import as_list, first_attr, get_field
20
21
  from ..model.vlans import build_vlan_info, normalize_networks
21
22
  from .config import Config
22
23
 
@@ -38,47 +39,18 @@ def _cache_dir() -> Path:
38
39
  return resolve_cache_dir(".cache/unifi_network_maps")
39
40
 
40
41
 
41
- def _device_attr(device: object, name: str) -> object | None:
42
- if isinstance(device, dict):
43
- return device.get(name)
44
- return getattr(device, name, None)
45
-
46
-
47
- def _first_attr(device: object, *names: str) -> object | None:
48
- for name in names:
49
- value = _device_attr(device, name)
50
- if value is not None:
51
- return value
52
- return None
53
-
54
-
55
- def _as_list(value: object | None) -> list[object]:
56
- if value is None:
57
- return []
58
- if isinstance(value, list):
59
- return value
60
- if isinstance(value, dict):
61
- return [value]
62
- if isinstance(value, str | bytes):
63
- return []
64
- try:
65
- return list(value) # type: ignore[arg-type]
66
- except TypeError:
67
- return []
68
-
69
-
70
42
  def _serialize_lldp_entry(entry: object) -> dict[str, object]:
71
43
  return {
72
- "chassis_id": _first_attr(entry, "chassis_id", "chassisId"),
73
- "port_id": _first_attr(entry, "port_id", "portId"),
74
- "port_desc": _first_attr(entry, "port_desc", "portDesc", "port_descr", "portDescr"),
75
- "local_port_name": _first_attr(entry, "local_port_name", "localPortName"),
76
- "local_port_idx": _first_attr(entry, "local_port_idx", "localPortIdx"),
44
+ "chassis_id": first_attr(entry, "chassis_id", "chassisId"),
45
+ "port_id": first_attr(entry, "port_id", "portId"),
46
+ "port_desc": first_attr(entry, "port_desc", "portDesc", "port_descr", "portDescr"),
47
+ "local_port_name": first_attr(entry, "local_port_name", "localPortName"),
48
+ "local_port_idx": first_attr(entry, "local_port_idx", "localPortIdx"),
77
49
  }
78
50
 
79
51
 
80
52
  def _serialize_lldp_entries(value: object | None) -> list[dict[str, object]]:
81
- entries = _as_list(value)
53
+ entries = as_list(value)
82
54
  serialized: list[dict[str, object]] = []
83
55
  for entry in entries:
84
56
  data = _serialize_lldp_entry(entry)
@@ -88,7 +60,7 @@ def _serialize_lldp_entries(value: object | None) -> list[dict[str, object]]:
88
60
 
89
61
 
90
62
  def _serialize_port_entry(entry: object) -> dict[str, object]:
91
- aggregation_group = _first_attr(
63
+ aggregation_group = first_attr(
92
64
  entry,
93
65
  "aggregation_group",
94
66
  "aggregation_id",
@@ -101,30 +73,47 @@ def _serialize_port_entry(entry: object) -> dict[str, object]:
101
73
  "aggregate",
102
74
  "aggregated_by",
103
75
  )
76
+ native_vlan = first_attr(
77
+ entry,
78
+ "native_networkconf_id",
79
+ "port_vlan",
80
+ "vlan",
81
+ "native_vlan",
82
+ "pvid",
83
+ )
84
+ tagged_vlans = first_attr(
85
+ entry,
86
+ "tagged_vlan_mgmt",
87
+ "tagged_vlans",
88
+ "vlan_trunk_mgmt",
89
+ "allowed_vlans",
90
+ )
104
91
  return {
105
- "port_idx": _first_attr(entry, "port_idx", "portIdx"),
106
- "name": _first_attr(entry, "name"),
107
- "ifname": _first_attr(entry, "ifname"),
108
- "speed": _first_attr(entry, "speed"),
92
+ "port_idx": first_attr(entry, "port_idx", "portIdx"),
93
+ "name": first_attr(entry, "name"),
94
+ "ifname": first_attr(entry, "ifname"),
95
+ "speed": first_attr(entry, "speed"),
109
96
  "aggregation_group": aggregation_group,
110
- "port_poe": _first_attr(entry, "port_poe"),
111
- "poe_enable": _first_attr(entry, "poe_enable"),
112
- "poe_good": _first_attr(entry, "poe_good"),
113
- "poe_power": _first_attr(entry, "poe_power"),
97
+ "port_poe": first_attr(entry, "port_poe"),
98
+ "poe_enable": first_attr(entry, "poe_enable"),
99
+ "poe_good": first_attr(entry, "poe_good"),
100
+ "poe_power": first_attr(entry, "poe_power"),
101
+ "native_vlan": native_vlan,
102
+ "tagged_vlans": tagged_vlans,
114
103
  }
115
104
 
116
105
 
117
106
  def _serialize_port_table(value: object | None) -> list[dict[str, object]]:
118
- return [_serialize_port_entry(entry) for entry in _as_list(value)]
107
+ return [_serialize_port_entry(entry) for entry in as_list(value)]
119
108
 
120
109
 
121
110
  def _serialize_uplink(value: object | None) -> dict[str, object] | None:
122
111
  if value is None:
123
112
  return None
124
113
  data = {
125
- "uplink_mac": _first_attr(value, "uplink_mac", "uplink_device_mac"),
126
- "uplink_device_name": _first_attr(value, "uplink_device_name", "uplink_name"),
127
- "uplink_remote_port": _first_attr(value, "uplink_remote_port", "port_idx"),
114
+ "uplink_mac": first_attr(value, "uplink_mac", "uplink_device_mac"),
115
+ "uplink_device_name": first_attr(value, "uplink_device_name", "uplink_name"),
116
+ "uplink_remote_port": first_attr(value, "uplink_remote_port", "port_idx"),
128
117
  }
129
118
  if any(item is not None for item in data.values()):
130
119
  return data
@@ -132,36 +121,36 @@ def _serialize_uplink(value: object | None) -> dict[str, object] | None:
132
121
 
133
122
 
134
123
  def _device_lldp_value(device: object) -> object | None:
135
- lldp_info = _device_attr(device, "lldp_info")
124
+ lldp_info = get_field(device, "lldp_info")
136
125
  if lldp_info is None:
137
- lldp_info = _device_attr(device, "lldp")
126
+ lldp_info = get_field(device, "lldp")
138
127
  if lldp_info is None:
139
- lldp_info = _device_attr(device, "lldp_table")
128
+ lldp_info = get_field(device, "lldp_table")
140
129
  return lldp_info
141
130
 
142
131
 
143
132
  def _device_uplink_fields(device: object) -> dict[str, object | None]:
144
133
  return {
145
- "uplink": _serialize_uplink(_device_attr(device, "uplink")),
146
- "last_uplink": _serialize_uplink(_device_attr(device, "last_uplink")),
147
- "uplink_mac": _first_attr(device, "uplink_mac", "uplink_device_mac"),
148
- "uplink_device_name": _device_attr(device, "uplink_device_name"),
149
- "uplink_remote_port": _device_attr(device, "uplink_remote_port"),
150
- "last_uplink_mac": _device_attr(device, "last_uplink_mac"),
134
+ "uplink": _serialize_uplink(get_field(device, "uplink")),
135
+ "last_uplink": _serialize_uplink(get_field(device, "last_uplink")),
136
+ "uplink_mac": first_attr(device, "uplink_mac", "uplink_device_mac"),
137
+ "uplink_device_name": get_field(device, "uplink_device_name"),
138
+ "uplink_remote_port": get_field(device, "uplink_remote_port"),
139
+ "last_uplink_mac": get_field(device, "last_uplink_mac"),
151
140
  }
152
141
 
153
142
 
154
143
  def _serialize_device_for_cache(device: object) -> dict[str, object]:
155
144
  payload = {
156
- "name": _device_attr(device, "name"),
157
- "model_name": _device_attr(device, "model_name"),
158
- "model": _device_attr(device, "model"),
159
- "mac": _device_attr(device, "mac"),
160
- "ip": _first_attr(device, "ip", "ip_address"),
161
- "type": _first_attr(device, "type", "device_type"),
162
- "displayable_version": _first_attr(device, "displayable_version", "version"),
145
+ "name": get_field(device, "name"),
146
+ "model_name": get_field(device, "model_name"),
147
+ "model": get_field(device, "model"),
148
+ "mac": get_field(device, "mac"),
149
+ "ip": first_attr(device, "ip", "ip_address"),
150
+ "type": first_attr(device, "type", "device_type"),
151
+ "displayable_version": first_attr(device, "displayable_version", "version"),
163
152
  "lldp_info": _serialize_lldp_entries(_device_lldp_value(device)),
164
- "port_table": _serialize_port_table(_device_attr(device, "port_table")),
153
+ "port_table": _serialize_port_table(get_field(device, "port_table")),
165
154
  }
166
155
  payload.update(_device_uplink_fields(device))
167
156
  return payload
@@ -173,10 +162,11 @@ def _serialize_devices_for_cache(devices: Sequence[object]) -> list[dict[str, ob
173
162
 
174
163
  def _serialize_network_for_cache(network: object) -> dict[str, object]:
175
164
  return {
176
- "name": _first_attr(network, "name", "network_name", "networkName"),
177
- "vlan": _first_attr(network, "vlan", "vlan_id", "vlanId", "vlanid"),
178
- "vlan_enabled": _first_attr(network, "vlan_enabled", "vlanEnabled"),
179
- "purpose": _first_attr(network, "purpose"),
165
+ "_id": first_attr(network, "_id", "id", "network_id", "networkId"),
166
+ "name": first_attr(network, "name", "network_name", "networkName"),
167
+ "vlan": first_attr(network, "vlan", "vlan_id", "vlanId", "vlanid"),
168
+ "vlan_enabled": first_attr(network, "vlan_enabled", "vlanEnabled"),
169
+ "purpose": first_attr(network, "purpose"),
180
170
  }
181
171
 
182
172
 
@@ -306,7 +296,7 @@ def _retry_attempts() -> int:
306
296
  if not value:
307
297
  return 2
308
298
  if value.isdigit():
309
- return max(1, int(value))
299
+ return min(max(1, int(value)), 20)
310
300
  logger.warning("Invalid UNIFI_RETRY_ATTEMPTS value: %s", value)
311
301
  return 2
312
302
 
@@ -316,7 +306,7 @@ def _retry_backoff_seconds() -> float:
316
306
  if not value:
317
307
  return 0.5
318
308
  try:
319
- return max(0.0, float(value))
309
+ return min(max(0.0, float(value)), 60.0)
320
310
  except ValueError:
321
311
  logger.warning("Invalid UNIFI_RETRY_BACKOFF_SECONDS value: %s", value)
322
312
  return 0.5
@@ -327,7 +317,7 @@ def _request_timeout_seconds() -> float | None:
327
317
  if not value:
328
318
  return None
329
319
  try:
330
- return max(0.0, float(value))
320
+ return min(max(0.0, float(value)), 300.0)
331
321
  except ValueError:
332
322
  logger.warning("Invalid UNIFI_REQUEST_TIMEOUT_SECONDS value: %s", value)
333
323
  return None
@@ -394,15 +384,12 @@ def fetch_devices(
394
384
  site_name = site or config.site
395
385
  ttl_seconds = _cache_ttl_seconds()
396
386
  cache_path = _cache_dir() / f"devices_{_cache_key(config.url, site_name, str(detailed))}.json"
397
- if use_cache and _is_cache_dir_safe(cache_path.parent):
387
+ cache_safe = use_cache and _is_cache_dir_safe(cache_path.parent)
388
+ if cache_safe:
398
389
  cached = _load_cache(cache_path, ttl_seconds)
399
- stale_cached, cache_age = _load_cache_with_age(cache_path)
400
- else:
401
- cached = None
402
- stale_cached, cache_age = None, None
403
- if cached is not None:
404
- logger.debug("Using cached devices (%d)", len(cached))
405
- return cached
390
+ if cached is not None:
391
+ logger.debug("Using cached devices (%d)", len(cached))
392
+ return cached
406
393
 
407
394
  try:
408
395
  controller = _init_controller(config, is_udm_pro=True)
@@ -416,6 +403,7 @@ def fetch_devices(
416
403
  try:
417
404
  devices = _call_with_retries("device fetch", _fetch)
418
405
  except Exception as exc: # noqa: BLE001 - fallback to cache
406
+ stale_cached, cache_age = _load_cache_with_age(cache_path) if cache_safe else (None, None)
419
407
  if stale_cached is not None:
420
408
  logger.warning(
421
409
  "Device fetch failed; using stale cache (%ds old): %s",
@@ -445,15 +433,12 @@ def fetch_clients(
445
433
  site_name = site or config.site
446
434
  ttl_seconds = _cache_ttl_seconds()
447
435
  cache_path = _cache_dir() / f"clients_{_cache_key(config.url, site_name)}.json"
448
- if use_cache and _is_cache_dir_safe(cache_path.parent):
436
+ cache_safe = use_cache and _is_cache_dir_safe(cache_path.parent)
437
+ if cache_safe:
449
438
  cached = _load_cache(cache_path, ttl_seconds)
450
- stale_cached, cache_age = _load_cache_with_age(cache_path)
451
- else:
452
- cached = None
453
- stale_cached, cache_age = None, None
454
- if cached is not None:
455
- logger.debug("Using cached clients (%d)", len(cached))
456
- return cached
439
+ if cached is not None:
440
+ logger.debug("Using cached clients (%d)", len(cached))
441
+ return cached
457
442
 
458
443
  try:
459
444
  controller = _init_controller(config, is_udm_pro=True)
@@ -467,6 +452,7 @@ def fetch_clients(
467
452
  try:
468
453
  clients = _call_with_retries("client fetch", _fetch)
469
454
  except Exception as exc: # noqa: BLE001 - fallback to cache
455
+ stale_cached, cache_age = _load_cache_with_age(cache_path) if cache_safe else (None, None)
470
456
  if stale_cached is not None:
471
457
  logger.warning(
472
458
  "Client fetch failed; using stale cache (%ds old): %s",
@@ -496,15 +482,12 @@ def fetch_networks(
496
482
  site_name = site or config.site
497
483
  ttl_seconds = _cache_ttl_seconds()
498
484
  cache_path = _cache_dir() / f"networks_{_cache_key(config.url, site_name)}.json"
499
- if use_cache and _is_cache_dir_safe(cache_path.parent):
485
+ cache_safe = use_cache and _is_cache_dir_safe(cache_path.parent)
486
+ if cache_safe:
500
487
  cached = _load_cache(cache_path, ttl_seconds)
501
- stale_cached, cache_age = _load_cache_with_age(cache_path)
502
- else:
503
- cached = None
504
- stale_cached, cache_age = None, None
505
- if cached is not None:
506
- logger.debug("Using cached networks (%d)", len(cached))
507
- return cached
488
+ if cached is not None:
489
+ logger.debug("Using cached networks (%d)", len(cached))
490
+ return cached
508
491
 
509
492
  try:
510
493
  controller = _init_controller(config, is_udm_pro=True)
@@ -520,6 +503,7 @@ def fetch_networks(
520
503
  try:
521
504
  networks = _call_with_retries("network fetch", _fetch)
522
505
  except Exception as exc: # noqa: BLE001 - fallback to cache
506
+ stale_cached, cache_age = _load_cache_with_age(cache_path) if cache_safe else (None, None)
523
507
  if stale_cached is not None:
524
508
  logger.warning(
525
509
  "Network fetch failed; using stale cache (%ds old): %s",
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon wifi as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 160">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 45, 126)">
5
+ <g transform="translate(-12,-12) scale(2)">
6
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M1.37128 8.1434C7.22915 2.28553 16.7266 2.28553 22.5845 8.1434C22.8774 8.43628 22.8774 8.91114 22.5845 9.20404L22.0542 9.73441C21.9135 9.87507 21.7228 9.9541 21.5238 9.9541C21.3249 9.95411 21.1341 9.87509 20.9935 9.73443C16.0143 4.75525 7.94146 4.75525 2.96227 9.73443C2.82162 9.87509 2.63084 9.95411 2.43193 9.9541C2.23301 9.9541 2.04224 9.87507 1.90159 9.73441L1.37126 9.20404C1.07839 8.91114 1.0784 8.43628 1.37128 8.1434ZM4.55326 11.3254C8.65377 7.22493 15.302 7.22493 19.4025 11.3254C19.6954 11.6183 19.6954 12.0932 19.4025 12.3861L18.8722 12.9164C18.7316 13.0571 18.5408 13.1361 18.3419 13.1361C18.143 13.1361 17.9522 13.0571 17.8115 12.9164C14.5897 9.6946 9.3661 9.6946 6.14427 12.9164C5.85139 13.2093 5.37652 13.2093 5.08362 12.9164L4.55327 12.3861C4.41262 12.2455 4.33359 12.0547 4.33359 11.8558C4.33359 11.6569 4.41261 11.4661 4.55326 11.3254ZM7.75733 14.5074C10.1005 12.1642 13.8995 12.1642 16.2426 14.5074C16.5355 14.8003 16.5355 15.2751 16.2426 15.568L15.7123 16.0983C15.5716 16.239 15.3809 16.318 15.182 16.318C14.983 16.318 14.7923 16.239 14.6516 16.0983C13.1872 14.6339 10.8128 14.6339 9.34832 16.0983C9.05543 16.3912 8.58055 16.3912 8.28766 16.0983L7.75733 15.568C7.46444 15.2751 7.46444 14.8003 7.75733 14.5074ZM10.9393 17.6893C11.5251 17.1036 12.4748 17.1036 13.0606 17.6893C13.3535 17.9822 13.3535 18.4571 13.0606 18.75L12.5303 19.2803C12.3896 19.421 12.1989 19.5 12 19.5C11.8011 19.5 11.6103 19.421 11.4696 19.2803L10.9393 18.75C10.6464 18.4571 10.6464 17.9822 10.9393 17.6893Z" fill="#DECAL0"/>
7
+ </g>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon video-camera as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" fill="#DECAL0"/>
7
+ </g>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon computer-desktop as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M2.25 5.25C2.25 3.59315 3.59315 2.25 5.25 2.25H18.75C20.4069 2.25 21.75 3.59315 21.75 5.25V15C21.75 16.6569 20.4069 18 18.75 18H15.75V18.2574C15.75 18.8541 15.9871 19.4264 16.409 19.8483L17.0303 20.4697C17.2448 20.6842 17.309 21.0068 17.1929 21.287C17.0768 21.5673 16.8033 21.75 16.5 21.75H7.5C7.19665 21.75 6.92318 21.5673 6.80709 21.287C6.691 21.0068 6.75517 20.6842 6.96967 20.4697L7.59099 19.8484C8.01295 19.4264 8.25 18.8541 8.25 18.2574V18H5.25C3.59315 18 2.25 16.6569 2.25 15V5.25ZM3.75 5.25V12.75C3.75 13.5784 4.42157 14.25 5.25 14.25H18.75C19.5784 14.25 20.25 13.5784 20.25 12.75V5.25C20.25 4.42157 19.5784 3.75 18.75 3.75H5.25C4.42157 3.75 3.75 4.42157 3.75 5.25Z" fill="#DECAL0"/>
7
+ </g>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon puzzle-piece as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 0 1-.657.643 48.39 48.39 0 0 0-4.163.3c-1.48.183-2.55 1.465-2.55 2.888v0c0 .426.084.84.248 1.216l3.553 8.088a2.25 2.25 0 0 0 2.062 1.348h4.514a2.25 2.25 0 0 0 2.062-1.348l3.553-8.088c.164-.376.248-.79.248-1.216v0c0-1.423-1.07-2.705-2.55-2.888a48.39 48.39 0 0 0-4.163-.3.64.64 0 0 1-.657-.643v0Z" fill="#DECAL0"/>
7
+ <path d="M9.375 15a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm0-3a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm1.5 1.5a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm1.5-1.5a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm1.5 0a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm1.5 1.5a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm0 1.5a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Z" fill="#DECAL0"/>
8
+ </g>
9
+ </g>
10
+ </svg>
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon globe-alt as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path d="M21.7214 12.7517C21.7404 12.5036 21.75 12.2529 21.75 11.9999C21.75 10.4758 21.4003 9.03328 20.7767 7.74835C19.5396 8.92269 18.0671 9.85146 16.4374 10.4565C16.4789 10.9655 16.5 11.4803 16.5 11.9999C16.5 13.1011 16.4051 14.1802 16.2229 15.2293C18.2163 14.7277 20.0717 13.8792 21.7214 12.7517Z" fill="#DECAL0"/>
7
+ <path d="M14.6343 15.5501C14.874 14.4043 15 13.2168 15 11.9999C15 11.6315 14.9885 11.2659 14.9657 10.9032C14.0141 11.1299 13.021 11.2499 12 11.2499C10.979 11.2499 9.98594 11.1299 9.0343 10.9032C9.01155 11.2659 9 11.6315 9 11.9999C9 13.2168 9.12601 14.4043 9.3657 15.5501C10.2246 15.6817 11.1043 15.7499 12 15.7499C12.8957 15.7499 13.7754 15.6817 14.6343 15.5501Z" fill="#DECAL0"/>
8
+ <path d="M9.77224 17.119C10.5028 17.2054 11.2462 17.2499 12 17.2499C12.7538 17.2499 13.4972 17.2054 14.2278 17.119C13.714 18.7746 12.9575 20.3235 12 21.724C11.0425 20.3235 10.286 18.7746 9.77224 17.119Z" fill="#DECAL0"/>
9
+ <path d="M7.77705 15.2293C7.59493 14.1802 7.5 13.1011 7.5 11.9999C7.5 11.4803 7.52114 10.9655 7.56261 10.4565C5.93286 9.85146 4.46039 8.92269 3.22333 7.74835C2.59973 9.03328 2.25 10.4758 2.25 11.9999C2.25 12.2529 2.25964 12.5036 2.27856 12.7517C3.92826 13.8792 5.78374 14.7277 7.77705 15.2293Z" fill="#DECAL0"/>
10
+ <path d="M21.3561 14.7525C20.3404 18.2104 17.4597 20.8705 13.8776 21.5693C14.744 20.1123 15.4185 18.5278 15.8664 16.8508C17.8263 16.44 19.6736 15.7231 21.3561 14.7525Z" fill="#DECAL0"/>
11
+ <path d="M2.64395 14.7525C4.32642 15.7231 6.17372 16.44 8.13356 16.8508C8.58146 18.5278 9.25602 20.1123 10.1224 21.5693C6.54027 20.8705 3.65964 18.2104 2.64395 14.7525Z" fill="#DECAL0"/>
12
+ <path d="M13.8776 2.43055C16.3991 2.92245 18.5731 4.3862 19.9937 6.41599C18.9351 7.48484 17.6637 8.34251 16.2483 8.92017C15.862 6.58282 15.0435 4.39132 13.8776 2.43055Z" fill="#DECAL0"/>
13
+ <path d="M12 2.27588C13.4287 4.36548 14.4097 6.78537 14.805 9.39744C13.9083 9.62756 12.9684 9.74993 12 9.74993C11.0316 9.74993 10.0917 9.62756 9.19503 9.39744C9.5903 6.78537 10.5713 4.36548 12 2.27588Z" fill="#DECAL0"/>
14
+ <path d="M10.1224 2.43055C8.95648 4.39132 8.13795 6.58282 7.75171 8.92017C6.33629 8.34251 5.06489 7.48484 4.00635 6.41599C5.42689 4.3862 7.60085 2.92245 10.1224 2.43055Z" fill="#DECAL0"/>
15
+ </g>
16
+ </g>
17
+ </svg>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon signal as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path d="M9.348 14.652a3.75 3.75 0 0 1 0-5.304m5.304 0a3.75 3.75 0 0 1 0 5.304m-7.425 2.121a6.75 6.75 0 0 1 0-9.546m9.546 0a6.75 6.75 0 0 1 0 9.546M5.106 18.894c-3.808-3.807-3.808-9.98 0-13.788m13.788 0c3.808 3.807 3.808 9.98 0 13.788M12 12h.008v.008H12V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" fill="#DECAL0"/>
7
+ </g>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon server-stack as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z" fill="#DECAL0"/>
7
+ </g>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon server as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path d="M4.07993 5.22701C4.43013 3.91375 5.61948 3 6.97863 3H17.0214C18.3805 3 19.5699 3.91375 19.9201 5.22701L22.0338 13.1535C21.1346 12.4318 19.9927 12 18.75 12H5.25C4.00727 12 2.86538 12.4318 1.96619 13.1535L4.07993 5.22701Z" fill="#DECAL0"/>
7
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 13.5C3.17893 13.5 1.5 15.1789 1.5 17.25C1.5 19.3211 3.17893 21 5.25 21H18.75C20.8211 21 22.5 19.3211 22.5 17.25C22.5 15.1789 20.8211 13.5 18.75 13.5H5.25ZM15.75 18C16.1642 18 16.5 17.6642 16.5 17.25C16.5 16.8358 16.1642 16.5 15.75 16.5C15.3358 16.5 15 16.8358 15 17.25C15 17.6642 15.3358 18 15.75 18ZM19.5 17.25C19.5 17.6642 19.1642 18 18.75 18C18.3358 18 18 17.6642 18 17.25C18 16.8358 18.3358 16.5 18.75 16.5C19.1642 16.5 19.5 16.8358 19.5 17.25Z" fill="#DECAL0"/>
8
+ </g>
9
+ </g>
10
+ </svg>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon device-phone-mobile as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path d="M10.5 18.75a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 0-1.5h-3Z" fill="#DECAL0"/>
7
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M8.625.75A3.375 3.375 0 0 0 5.25 4.125v15.75a3.375 3.375 0 0 0 3.375 3.375h6.75a3.375 3.375 0 0 0 3.375-3.375V4.125A3.375 3.375 0 0 0 15.375.75h-6.75ZM7.5 4.125C7.5 3.504 8.004 3 8.625 3H9.75v.375c0 .621.504 1.125 1.125 1.125h2.25c.621 0 1.125-.504 1.125-1.125V3h1.125c.621 0 1.125.504 1.125 1.125v15.75c0 .621-.504 1.125-1.125 1.125h-6.75a1.125 1.125 0 0 1-1.125-1.125V4.125Z" fill="#DECAL0"/>
8
+ </g>
9
+ </g>
10
+ </svg>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon printer as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.875 1.5C6.839 1.5 6 2.34 6 3.375v2.99c-.426.053-.851.11-1.274.174-1.454.218-2.476 1.483-2.476 2.917v6.294a3 3 0 0 0 3 3h.27l-.155 1.705A1.875 1.875 0 0 0 7.232 22.5h9.536a1.875 1.875 0 0 0 1.867-2.045l-.155-1.705h.27a3 3 0 0 0 3-3V9.456c0-1.434-1.022-2.7-2.476-2.917A48.716 48.716 0 0 0 18 6.366V3.375c0-1.036-.84-1.875-1.875-1.875h-8.25ZM16.5 6.205v-2.83a.375.375 0 0 0-.375-.375h-8.25a.375.375 0 0 0-.375.375v2.83a49.353 49.353 0 0 1 9 0Zm-.217 8.265c.178.018.317.16.333.337l.526 5.784a.375.375 0 0 1-.374.409H7.232a.375.375 0 0 1-.374-.409l.526-5.784a.373.373 0 0 1 .333-.337 41.741 41.741 0 0 1 8.566 0Zm.967-3.97a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75H18a.75.75 0 0 1-.75-.75V10.5Zm-3 0a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75H15a.75.75 0 0 1-.75-.75V10.5Z" fill="#DECAL0"/>
7
+ </g>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon speaker-wave as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path d="M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 0 0 1.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06ZM18.584 5.106a.75.75 0 0 1 1.06 0c3.808 3.807 3.808 9.98 0 13.788a.75.75 0 0 1-1.06-1.06 8.25 8.25 0 0 0 0-11.668.75.75 0 0 1 0-1.06Z" fill="#DECAL0"/>
7
+ <path d="M15.932 7.757a.75.75 0 0 1 1.061 0 6 6 0 0 1 0 8.486.75.75 0 0 1-1.06-1.061 4.5 4.5 0 0 0 0-6.364.75.75 0 0 1 0-1.06Z" fill="#DECAL0"/>
8
+ </g>
9
+ </g>
10
+ </svg>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon server as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path d="M4.07993 5.22701C4.43013 3.91375 5.61948 3 6.97863 3H17.0214C18.3805 3 19.5699 3.91375 19.9201 5.22701L22.0338 13.1535C21.1346 12.4318 19.9927 12 18.75 12H5.25C4.00727 12 2.86538 12.4318 1.96619 13.1535L4.07993 5.22701Z" fill="#DECAL0"/>
7
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 13.5C3.17893 13.5 1.5 15.1789 1.5 17.25C1.5 19.3211 3.17893 21 5.25 21H18.75C20.8211 21 22.5 19.3211 22.5 17.25C22.5 15.1789 20.8211 13.5 18.75 13.5H5.25ZM15.75 18C16.1642 18 16.5 17.6642 16.5 17.25C16.5 16.8358 16.1642 16.5 15.75 16.5C15.3358 16.5 15 16.8358 15 17.25C15 17.6642 15.3358 18 15.75 18ZM19.5 17.25C19.5 17.6642 19.1642 18 18.75 18C18.3358 18 18 17.6642 18 17.25C18 16.8358 18.3358 16.5 18.75 16.5C19.1642 16.5 19.5 16.8358 19.5 17.25Z" fill="#DECAL0"/>
8
+ </g>
9
+ </g>
10
+ </svg>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon tv as isometric decal (30° perspective) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 66">
4
+ <g transform="matrix(0.866, 0.5, -0.866, 0.5, 30, 38)">
5
+ <g transform="translate(-12,-12) scale(1)">
6
+ <path d="M19.5 6h-15v9h15V6Z" fill="#DECAL0"/>
7
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3.375 3C2.339 3 1.5 3.84 1.5 4.875v11.25C1.5 17.16 2.34 18 3.375 18H9.75v1.5H6A.75.75 0 0 0 6 21h12a.75.75 0 0 0 0-1.5h-3.75V18h6.375c1.035 0 1.875-.84 1.875-1.875V4.875C22.5 3.839 21.66 3 20.625 3H3.375Zm0 13.5h17.25a.375.375 0 0 0 .375-.375V4.875a.375.375 0 0 0-.375-.375H3.375A.375.375 0 0 0 3 4.875v11.25c0 .207.168.375.375.375Z" fill="#DECAL0"/>
8
+ </g>
9
+ </g>
10
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon wifi (flat) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path fill-rule="evenodd" d="M1.371 8.143c5.858-5.857 15.356-5.857 21.213 0a.75.75 0 0 1 0 1.061.75.75 0 0 1-1.06 0 13.5 13.5 0 0 0-19.092 0 .75.75 0 0 1-1.06-1.06Zm3.182 3.182a10.5 10.5 0 0 1 14.85 0 .75.75 0 1 1-1.061 1.06 9 9 0 0 0-12.728 0 .75.75 0 1 1-1.06-1.06Zm3.182 3.182a6 6 0 0 1 8.486 0 .75.75 0 1 1-1.061 1.06 4.5 4.5 0 0 0-6.364 0 .75.75 0 0 1-1.06-1.06Zm3.182 3.182a1.5 1.5 0 0 1 2.121 0 .75.75 0 1 1-1.061 1.06.75.75 0 0 1 0-1.06Z" clip-rule="evenodd"/>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon video-camera (mini) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon computer-desktop (flat) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path fill-rule="evenodd" d="M2.25 5.25a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3V15a3 3 0 0 1-3 3h-3v.257c0 .597.237 1.17.659 1.591l.621.622a.75.75 0 0 1-.53 1.28h-9a.75.75 0 0 1-.53-1.28l.621-.622a2.25 2.25 0 0 0 .659-1.59V18h-3a3 3 0 0 1-3-3V5.25Zm1.5 0v7.5a1.5 1.5 0 0 0 1.5 1.5h13.5a1.5 1.5 0 0 0 1.5-1.5v-7.5a1.5 1.5 0 0 0-1.5-1.5H5.25a1.5 1.5 0 0 0-1.5 1.5Z" clip-rule="evenodd"/>
5
+ </svg>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon puzzle-piece (mini) - representing game/entertainment -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 0 1-.657.643 48.39 48.39 0 0 0-4.163.3c-1.48.183-2.55 1.465-2.55 2.888v0c0 .426.084.84.248 1.216l3.553 8.088a2.25 2.25 0 0 0 2.062 1.348h4.514a2.25 2.25 0 0 0 2.062-1.348l3.553-8.088c.164-.376.248-.79.248-1.216v0c0-1.423-1.07-2.705-2.55-2.888a48.39 48.39 0 0 0-4.163-.3.64.64 0 0 1-.657-.643v0Z"/>
5
+ <path d="M9.375 15a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm0-3a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm1.5 1.5a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm1.5-1.5a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm1.5 0a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm1.5 1.5a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Zm0 1.5a.375.375 0 1 1 0-.75.375.375 0 0 1 0 .75Z"/>
6
+ </svg>
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon globe-alt (flat) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path d="M21.7214 12.7517C21.7404 12.5036 21.75 12.2529 21.75 11.9999C21.75 10.4758 21.4003 9.03328 20.7767 7.74835C19.5396 8.92269 18.0671 9.85146 16.4374 10.4565C16.4789 10.9655 16.5 11.4803 16.5 11.9999C16.5 13.1011 16.4051 14.1802 16.2229 15.2293C18.2163 14.7277 20.0717 13.8792 21.7214 12.7517Z"/>
5
+ <path d="M14.6343 15.5501C14.874 14.4043 15 13.2168 15 11.9999C15 11.6315 14.9885 11.2659 14.9657 10.9032C14.0141 11.1299 13.021 11.2499 12 11.2499C10.979 11.2499 9.98594 11.1299 9.0343 10.9032C9.01155 11.2659 9 11.6315 9 11.9999C9 13.2168 9.12601 14.4043 9.3657 15.5501C10.2246 15.6817 11.1043 15.7499 12 15.7499C12.8957 15.7499 13.7754 15.6817 14.6343 15.5501Z"/>
6
+ <path d="M9.77224 17.119C10.5028 17.2054 11.2462 17.2499 12 17.2499C12.7538 17.2499 13.4972 17.2054 14.2278 17.119C13.714 18.7746 12.9575 20.3235 12 21.724C11.0425 20.3235 10.286 18.7746 9.77224 17.119Z"/>
7
+ <path d="M7.77705 15.2293C7.59493 14.1802 7.5 13.1011 7.5 11.9999C7.5 11.4803 7.52114 10.9655 7.56261 10.4565C5.93286 9.85146 4.46039 8.92269 3.22333 7.74835C2.59973 9.03328 2.25 10.4758 2.25 11.9999C2.25 12.2529 2.25964 12.5036 2.27856 12.7517C3.92826 13.8792 5.78374 14.7277 7.77705 15.2293Z"/>
8
+ <path d="M21.3561 14.7525C20.3404 18.2104 17.4597 20.8705 13.8776 21.5693C14.744 20.1123 15.4185 18.5278 15.8664 16.8508C17.8263 16.44 19.6736 15.7231 21.3561 14.7525Z"/>
9
+ <path d="M2.64395 14.7525C4.32642 15.7231 6.17372 16.44 8.13356 16.8508C8.58146 18.5278 9.25602 20.1123 10.1224 21.5693C6.54027 20.8705 3.65964 18.2104 2.64395 14.7525Z"/>
10
+ <path d="M13.8776 2.43055C16.3991 2.92245 18.5731 4.3862 19.9937 6.41599C18.9351 7.48484 17.6637 8.34251 16.2483 8.92017C15.862 6.58282 15.0435 4.39132 13.8776 2.43055Z"/>
11
+ <path d="M12 2.27588C13.4287 4.36548 14.4097 6.78537 14.805 9.39744C13.9083 9.62756 12.9684 9.74993 12 9.74993C11.0316 9.74993 10.0917 9.62756 9.19503 9.39744C9.5903 6.78537 10.5713 4.36548 12 2.27588Z"/>
12
+ <path d="M10.1224 2.43055C8.95648 4.39132 8.13795 6.58282 7.75171 8.92017C6.33629 8.34251 5.06489 7.48484 4.00635 6.41599C5.42689 4.3862 7.60085 2.92245 10.1224 2.43055Z"/>
13
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon signal (mini) - representing IoT/connected devices -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path d="M9.348 14.652a3.75 3.75 0 0 1 0-5.304m5.304 0a3.75 3.75 0 0 1 0 5.304m-7.425 2.121a6.75 6.75 0 0 1 0-9.546m9.546 0a6.75 6.75 0 0 1 0 9.546M5.106 18.894c-3.808-3.807-3.808-9.98 0-13.788m13.788 0c3.808 3.807 3.808 9.98 0 13.788M12 12h.008v.008H12V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"/>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon server-stack (mini) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"/>
5
+ </svg>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon server (flat) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path d="M4.08 5.227A3 3 0 0 1 6.979 3H17.02a3 3 0 0 1 2.9 2.227l2.113 7.926A5.228 5.228 0 0 0 18.75 12H5.25a5.228 5.228 0 0 0-3.284 1.153L4.08 5.227Z"/>
5
+ <path fill-rule="evenodd" d="M5.25 13.5a3.75 3.75 0 1 0 0 7.5h13.5a3.75 3.75 0 1 0 0-7.5H5.25Zm10.5 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm3.75-.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" clip-rule="evenodd"/>
6
+ </svg>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon device-phone-mobile (mini) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path d="M10.5 18.75a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 0-1.5h-3Z"/>
5
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M8.625.75A3.375 3.375 0 0 0 5.25 4.125v15.75a3.375 3.375 0 0 0 3.375 3.375h6.75a3.375 3.375 0 0 0 3.375-3.375V4.125A3.375 3.375 0 0 0 15.375.75h-6.75ZM7.5 4.125C7.5 3.504 8.004 3 8.625 3H9.75v.375c0 .621.504 1.125 1.125 1.125h2.25c.621 0 1.125-.504 1.125-1.125V3h1.125c.621 0 1.125.504 1.125 1.125v15.75c0 .621-.504 1.125-1.125 1.125h-6.75a1.125 1.125 0 0 1-1.125-1.125V4.125Z"/>
6
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon printer (mini) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.875 1.5C6.839 1.5 6 2.34 6 3.375v2.99c-.426.053-.851.11-1.274.174-1.454.218-2.476 1.483-2.476 2.917v6.294a3 3 0 0 0 3 3h.27l-.155 1.705A1.875 1.875 0 0 0 7.232 22.5h9.536a1.875 1.875 0 0 0 1.867-2.045l-.155-1.705h.27a3 3 0 0 0 3-3V9.456c0-1.434-1.022-2.7-2.476-2.917A48.716 48.716 0 0 0 18 6.366V3.375c0-1.036-.84-1.875-1.875-1.875h-8.25ZM16.5 6.205v-2.83a.375.375 0 0 0-.375-.375h-8.25a.375.375 0 0 0-.375.375v2.83a49.353 49.353 0 0 1 9 0Zm-.217 8.265c.178.018.317.16.333.337l.526 5.784a.375.375 0 0 1-.374.409H7.232a.375.375 0 0 1-.374-.409l.526-5.784a.373.373 0 0 1 .333-.337 41.741 41.741 0 0 1 8.566 0Zm.967-3.97a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75H18a.75.75 0 0 1-.75-.75V10.5Zm-3 0a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75H15a.75.75 0 0 1-.75-.75V10.5Z"/>
5
+ </svg>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon speaker-wave (mini) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path d="M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 0 0 1.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06ZM18.584 5.106a.75.75 0 0 1 1.06 0c3.808 3.807 3.808 9.98 0 13.788a.75.75 0 0 1-1.06-1.06 8.25 8.25 0 0 0 0-11.668.75.75 0 0 1 0-1.06Z"/>
5
+ <path d="M15.932 7.757a.75.75 0 0 1 1.061 0 6 6 0 0 1 0 8.486.75.75 0 0 1-1.06-1.061 4.5 4.5 0 0 0 0-6.364.75.75 0 0 1 0-1.06Z"/>
6
+ </svg>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon server-stack (flat) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path d="M5.25 2.25a3 3 0 0 0-3 3v4.5a3 3 0 0 0 3 3h13.5a3 3 0 0 0 3-3v-4.5a3 3 0 0 0-3-3H5.25Zm0 1.5h13.5a1.5 1.5 0 0 1 1.5 1.5v4.5a1.5 1.5 0 0 1-1.5 1.5H5.25a1.5 1.5 0 0 1-1.5-1.5v-4.5a1.5 1.5 0 0 1 1.5-1.5ZM5.25 11.25a3 3 0 0 0-3 3v4.5a3 3 0 0 0 3 3h13.5a3 3 0 0 0 3-3v-4.5a3 3 0 0 0-3-3H5.25Zm0 1.5h13.5a1.5 1.5 0 0 1 1.5 1.5v4.5a1.5 1.5 0 0 1-1.5 1.5H5.25a1.5 1.5 0 0 1-1.5-1.5v-4.5a1.5 1.5 0 0 1 1.5-1.5Z"/>
5
+ <path d="M6 6.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM6 15.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"/>
6
+ </svg>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Heroicon tv (mini) -->
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#DECAL0">
4
+ <path d="M19.5 6h-15v9h15V6Z"/>
5
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3.375 3C2.339 3 1.5 3.84 1.5 4.875v11.25C1.5 17.16 2.34 18 3.375 18H9.75v1.5H6A.75.75 0 0 0 6 21h12a.75.75 0 0 0 0-1.5h-3.75V18h6.375c1.035 0 1.875-.84 1.875-1.875V4.875C22.5 3.839 21.66 3 20.625 3H3.375Zm0 13.5h17.25a.375.375 0 0 0 .375-.375V4.875a.375.375 0 0 0-.375-.375H3.375A.375.375 0 0 0 3 4.875v11.25c0 .207.168.375.375.375Z"/>
6
+ </svg>