Dev #11

Merged
serega404 merged 87 commits from dev into main 2026-05-25 03:22:55 +03:00
53 changed files with 1162 additions and 88 deletions
Showing only changes of commit 3b0bbfc858 - Show all commits
+22
View File
@@ -0,0 +1,22 @@
<!--
tags: [time, watch, clock, ring, alarm, control, operation, function, interface, management]
category: System
version: "1.1"
unicode: "ea04"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 13a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
<path d="M12 10l0 3l2 0" />
<path d="M7 4l-2.75 2" />
<path d="M17 4l2.75 2" />
</svg>

After

Width:  |  Height:  |  Size: 496 B

@@ -0,0 +1,21 @@
<!--
tags: [warning, danger, caution, risk, alert, triangle, control, operation, function, interface]
category: System
version: "1.0"
unicode: "ea06"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 9v4" />
<path d="M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.87l-8.106 -13.536a1.914 1.914 0 0 0 -3.274 0" />
<path d="M12 16h.01" />
</svg>

After

Width:  |  Height:  |  Size: 563 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [alarm, sound, notification, bell, control, operation, function, interface, management]
category: System
version: "1.0"
unicode: "ea35"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6" />
<path d="M9 17v1a3 3 0 0 0 6 0v-1" />
</svg>

After

Width:  |  Height:  |  Size: 501 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [read, dictionary, magazine, library, booklet, novel, book, file, paper, text]
category: Document
version: "1.50"
unicode: "efc5"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 4v16h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12" />
<path d="M19 16h-12a2 2 0 0 0 -2 2" />
<path d="M9 8h6" />
</svg>

After

Width:  |  Height:  |  Size: 482 B

+25
View File
@@ -0,0 +1,25 @@
<!--
tags: [education, learning, reading, school, library, books, file, paper, text, record]
category: Document
version: "1.52"
unicode: "eff2"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 5a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1l0 -14" />
<path d="M9 5a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1l0 -14" />
<path d="M5 8h4" />
<path d="M9 16h4" />
<path d="M13.803 4.56l2.184 -.53c.562 -.135 1.133 .19 1.282 .732l3.695 13.418a1.02 1.02 0 0 1 -.634 1.219l-.133 .041l-2.184 .53c-.562 .135 -1.133 -.19 -1.282 -.732l-3.695 -13.418a1.02 1.02 0 0 1 .634 -1.219l.133 -.041" />
<path d="M14 9l4 -1" />
<path d="M16 16l3.923 -.98" />
</svg>

After

Width:  |  Height:  |  Size: 872 B

+26
View File
@@ -0,0 +1,26 @@
<!--
tags: [flat, office, city, urban, scyscraper, architecture, construction, building, structure, property]
category: Buildings
version: "1.1"
unicode: "ea4f"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 21l18 0" />
<path d="M9 8l1 0" />
<path d="M9 12l1 0" />
<path d="M9 16l1 0" />
<path d="M14 8l1 0" />
<path d="M14 12l1 0" />
<path d="M14 16l1 0" />
<path d="M5 21v-16a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v16" />
</svg>

After

Width:  |  Height:  |  Size: 610 B

+21
View File
@@ -0,0 +1,21 @@
<!--
category: Design
tags: [energy, power, electricity, creativity, light, idea, bulb]
version: "1.0"
unicode: "ea51"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12h1m8 -9v1m8 8h1m-15.4 -6.4l.7 .7m12.1 -.7l-.7 .7" />
<path d="M9 16a5 5 0 1 1 6 0a3.5 3.5 0 0 0 -1 3a2 2 0 0 1 -4 0a3.5 3.5 0 0 0 -1 -3" />
<path d="M9.7 17l4.6 0" />
</svg>

After

Width:  |  Height:  |  Size: 518 B

@@ -0,0 +1,23 @@
<!--
tags: [date, day, plan, schedule, agenda, calender, calendar, event, control, operation]
category: System
version: "1.1"
unicode: "ea52"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" />
<path d="M16 3l0 4" />
<path d="M8 3l0 4" />
<path d="M4 11l16 0" />
<path d="M8 15h2v2h-2l0 -2" />
</svg>

After

Width:  |  Height:  |  Size: 558 B

+24
View File
@@ -0,0 +1,24 @@
<!--
tags: [date, day, plan, schedule, agenda, calender, calendar, control, operation, function]
category: System
version: "1.0"
unicode: "ea53"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12" />
<path d="M16 3v4" />
<path d="M8 3v4" />
<path d="M4 11h16" />
<path d="M11 15h1" />
<path d="M12 15v3" />
</svg>

After

Width:  |  Height:  |  Size: 568 B

+22
View File
@@ -0,0 +1,22 @@
<!--
tags: [statistics, diagram, graph, rhythm, data, analysis, chart, bar, visualization, analytics]
category: Charts
version: "1.0"
unicode: "ea59"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 13a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -6" />
<path d="M15 9a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -10" />
<path d="M9 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -14" />
<path d="M4 20h14" />
</svg>

After

Width:  |  Height:  |  Size: 668 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [statistics, diagram, graph, rhythm, data, analysis, chart, line, visualization, analytics]
category: Charts
version: "1.0"
unicode: "ea5c"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 19l16 0" />
<path d="M4 15l4 -6l4 2l4 -5l4 4" />
</svg>

After

Width:  |  Height:  |  Size: 428 B

@@ -0,0 +1,20 @@
<!--
tags: [accept, yes, tick, done, circle, check, round, circular, confirm, approve]
category: Shapes
version: "1.0"
unicode: "ea67"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M9 12l2 2l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 429 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [cancel, "no", circle, round, circular, geometry, form, figure, pattern, outline]
category: Shapes
version: "1.0"
unicode: "ea6a"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M10 10l4 4m0 -4l-4 4" />
</svg>

After

Width:  |  Height:  |  Size: 441 B

@@ -0,0 +1,24 @@
<!--
tags: [copy, items, clipboard, list, file, paper, text, record, information]
category: Document
version: "1.0"
unicode: "ea6d"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2" />
<path d="M9 5a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2" />
<path d="M9 12l.01 0" />
<path d="M13 12l2 0" />
<path d="M9 16l.01 0" />
<path d="M13 16l2 0" />
</svg>

After

Width:  |  Height:  |  Size: 631 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [time, watch, alarm, clock, control, operation, function, interface, management]
category: System
version: "1.0"
unicode: "ea70"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M12 7v5l3 3" />
</svg>

After

Width:  |  Height:  |  Size: 431 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [money, earn, salary, change, dollar, coin, store, purchase, shopping, retail]
category: E-commerce
version: "1.3"
unicode: "eb82"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M14.8 9a2 2 0 0 0 -1.8 -1h-2a2 2 0 1 0 0 4h2a2 2 0 1 1 0 4h-2a2 2 0 0 1 -1.8 -1" />
<path d="M12 7v10" />
</svg>

After

Width:  |  Height:  |  Size: 525 B

+22
View File
@@ -0,0 +1,22 @@
<!--
tags: [forbiddance, nixing, ban, interdicting, hand, stop, touch, action, motion, interaction]
category: Gestures
version: "1.10"
unicode: "ec2e"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5" />
<path d="M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5" />
<path d="M14 5.5a1.5 1.5 0 0 1 3 0v6.5" />
<path d="M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47" />
</svg>

After

Width:  |  Height:  |  Size: 725 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [house, dashboard, living, building, home, main, architecture, structure, start, construction]
category: Buildings
version: "1.0"
unicode: "eac1"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12l-2 0l9 -9l9 9l-2 0" />
<path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" />
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" />
</svg>

After

Width:  |  Height:  |  Size: 524 B

+20
View File
@@ -0,0 +1,20 @@
<!--
category: System
tags: [mail, gmail, email, envelope, post, inbox]
version: "1.0"
unicode: "eac4"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" />
<path d="M4 13h3l3 3h4l3 -3h3" />
</svg>

After

Width:  |  Height:  |  Size: 447 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [information, advice, news, tip, sign, info, circle, control, operation, round]
category: System
version: "1.0"
unicode: "eac5"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M12 9h.01" />
<path d="M11 12h1v4h1" />
</svg>

After

Width:  |  Height:  |  Size: 456 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [security, password, secure, lock, private, control, operation, protected, closed, function]
category: System
version: "1.0"
unicode: "eae2"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6" />
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0" />
<path d="M8 11v-4a4 4 0 1 1 8 0v4" />
</svg>

After

Width:  |  Height:  |  Size: 548 B

+21
View File
@@ -0,0 +1,21 @@
<!--
tags: [exit, shut, unplug, close, logout, control, operation, function, interface, management]
category: System
version: "1.4"
unicode: "eba8"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2" />
<path d="M9 12h12l-3 -3" />
<path d="M18 15l3 -3" />
</svg>

After

Width:  |  Height:  |  Size: 512 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [navigation, location, travel, pin, position, marker, map, attach, fix, mark]
category: Map
version: "1.0"
unicode: "eae8"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 11a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
<path d="M17.657 16.657l-4.243 4.243a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 1 1 11.314 0" />
</svg>

After

Width:  |  Height:  |  Size: 491 B

@@ -0,0 +1,19 @@
<!--
tags: [comment, chat, reply, message, circle, contact, conversation, round, circular, messaging]
category: Communication
version: "1.0"
unicode: "eaed"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 20l1.3 -3.9c-2.324 -3.437 -1.426 -7.872 2.1 -10.374c3.526 -2.501 8.59 -2.296 11.845 .48c3.255 2.777 3.695 7.266 1.029 10.501c-2.666 3.235 -7.615 4.215 -11.574 2.293l-4.7 1" />
</svg>

After

Width:  |  Height:  |  Size: 559 B

@@ -0,0 +1,21 @@
<!--
tags: [face, emoji, emotion, mood, neutral]
category: Mood
version: "1.0"
unicode: "eaf5"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M9 10l.01 0" />
<path d="M15 10l.01 0" />
</svg>

After

Width:  |  Height:  |  Size: 414 B

+27
View File
@@ -0,0 +1,27 @@
<!--
tags: [technology, ai, machine, bot, android, robot, entertainment, playing, recreation, fun]
category: Games
version: "1.53"
unicode: "f00b"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 6a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2l0 -4" />
<path d="M12 2v2" />
<path d="M9 12v9" />
<path d="M15 12v9" />
<path d="M5 16l4 -2" />
<path d="M15 14l4 2" />
<path d="M9 18h6" />
<path d="M10 8v.01" />
<path d="M14 8v.01" />
</svg>

After

Width:  |  Height:  |  Size: 646 B

+20
View File
@@ -0,0 +1,20 @@
<!--
category: System
tags: [find, magnifier, magnifying glass, search, look, seek, query, browse]
version: "1.0"
unicode: "eb1c"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
<path d="M21 21l-6 -6" />
</svg>

After

Width:  |  Height:  |  Size: 422 B

+19
View File
@@ -0,0 +1,19 @@
<!--
tags: [safety, protect, protection, shield, control, operation, function, interface, management]
category: System
version: "1.0"
unicode: "eb24"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3" />
</svg>

After

Width:  |  Height:  |  Size: 461 B

+19
View File
@@ -0,0 +1,19 @@
<!--
category: System
tags: [star, light, fire, shine, sparkles]
version: "2.1"
unicode: "f6d7"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2m0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2m-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6" />
</svg>

After

Width:  |  Height:  |  Size: 522 B

+19
View File
@@ -0,0 +1,19 @@
<!--
tags: [favorite, like, mark, bookmark, grade, star, control, operation, rating, function]
category: System
version: "1.0"
unicode: "eb2e"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873l-6.158 -3.245" />
</svg>

After

Width:  |  Height:  |  Size: 489 B

+22
View File
@@ -0,0 +1,22 @@
<!--
tags: [timer, time, watch, clock, run, race, stopwatch, control, operation, function]
category: System
version: "3.12"
unicode: "ff9b"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 13a7 7 0 1 0 14 0a7 7 0 0 0 -14 0" />
<path d="M14.5 10.5l-2.5 2.5" />
<path d="M17 8l1 -1" />
<path d="M14 3h-4" />
</svg>

After

Width:  |  Height:  |  Size: 489 B

+19
View File
@@ -0,0 +1,19 @@
<!--
tags: [dislike, bad, emotion, thumb, down, bottom, decrease, control, operation, fall]
category: System
version: "1.0"
unicode: "eb3b"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 13v-8a1 1 0 0 0 -1 -1h-2a1 1 0 0 0 -1 1v7a1 1 0 0 0 1 1h3a4 4 0 0 1 4 4v1a2 2 0 0 0 4 0v-5h3a2 2 0 0 0 2 -2l-1 -5a2 3 0 0 0 -2 -2h-7a3 3 0 0 0 -3 3" />
</svg>

After

Width:  |  Height:  |  Size: 518 B

+19
View File
@@ -0,0 +1,19 @@
<!--
tags: [like, emotion, good, love, thumb, top, increase, control, operation, rise]
category: System
version: "1.0"
unicode: "eb3c"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 11v8a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1h3a4 4 0 0 0 4 -4v-1a2 2 0 0 1 4 0v5h3a2 2 0 0 1 2 2l-1 5a2 3 0 0 1 -2 2h-7a3 3 0 0 1 -3 -3" />
</svg>

After

Width:  |  Height:  |  Size: 513 B

+24
View File
@@ -0,0 +1,24 @@
<!--
category: Sport
tags: [success, win, prize, winner, trophy]
version: "1.0"
unicode: "eb45"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 21l8 0" />
<path d="M12 17l0 4" />
<path d="M7 4l10 0" />
<path d="M17 4v8a5 5 0 0 1 -10 0v-8" />
<path d="M3 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M17 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
</svg>

After

Width:  |  Height:  |  Size: 525 B

+20
View File
@@ -0,0 +1,20 @@
<!--
tags: [person, account, user, control, operation, profile, member, function, interface, management]
category: System
version: "1.0"
unicode: "eb4d"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
<path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
</svg>

After

Width:  |  Height:  |  Size: 471 B

+22
View File
@@ -0,0 +1,22 @@
<!--
tags: [people, persons, accounts, users, control, operation, function, interface, management]
category: System
version: "1.7"
unicode: "ebf2"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 7a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" />
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>

After

Width:  |  Height:  |  Size: 550 B

+23
View File
@@ -0,0 +1,23 @@
<!--
tags: [earth, globe, global, language, union, world, location, navigation, geography, place]
category: Map
version: "1.0"
unicode: "eb54"
-->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M3.6 9h16.8" />
<path d="M3.6 15h16.8" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>

After

Width:  |  Height:  |  Size: 542 B

+15 -14
View File
@@ -2,6 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import AppIcon from '@/components/ui/AppIcon.vue'
const auth = useAuthStore() const auth = useAuthStore()
const route = useRoute() const route = useRoute()
@@ -9,22 +10,22 @@ const route = useRoute()
const navItems = computed(() => { const navItems = computed(() => {
const role = auth.user?.role ?? 'student' const role = auth.user?.role ?? 'student'
if (role === 'teacher') return [ if (role === 'teacher') return [
{ label: 'Дашборд', icon: '📊', to: '/teacher' }, { label: 'Дашборд', icon: 'chart-bar', to: '/teacher' },
{ label: 'Лекции', icon: '📖', to: '/teacher/lectures' }, { label: 'Лекции', icon: 'book-2', to: '/teacher/lectures' },
{ label: 'Аналитика',icon: '📈', to: '/teacher/analytics' }, { label: 'Аналитика', icon: 'chart-line', to: '/teacher/analytics' },
{ label: 'Профиль', icon: '👤', to: '/profile' }, { label: 'Профиль', icon: 'user', to: '/profile' },
] ]
if (role === 'admin') return [ if (role === 'admin') return [
{ label: 'Дашборд', icon: '🛡️', to: '/admin' }, { label: 'Дашборд', icon: 'shield', to: '/admin' },
{ label: 'Юзеры', icon: '👥', to: '/admin/users' }, { label: 'Юзеры', icon: 'users', to: '/admin/users' },
{ label: 'Лекции', icon: '📚', to: '/admin/lectures' }, { label: 'Лекции', icon: 'books', to: '/admin/lectures' },
{ label: 'ИИ', icon: '🤖', to: '/admin/llm-queue' }, { label: 'ИИ', icon: 'robot', to: '/admin/llm-queue' },
] ]
return [ return [
{ label: 'Главная', icon: '🏠', to: '/' }, { label: 'Главная', icon: 'home', to: '/' },
{ label: 'Лекции', icon: '📚', to: '/catalog' }, { label: 'Лекции', icon: 'books', to: '/catalog' },
{ label: 'Мои', icon: '📋', to: '/my-lectures' }, { label: 'Мои', icon: 'clipboard-list', to: '/my-lectures' },
{ label: 'Профиль', icon: '👤', to: '/profile' }, { label: 'Профиль', icon: 'user', to: '/profile' },
] ]
}) })
@@ -43,7 +44,7 @@ function isActive(to: string) {
class="bottom-nav-item" class="bottom-nav-item"
:class="{ active: isActive(item.to) }" :class="{ active: isActive(item.to) }"
> >
<span class="bottom-nav-icon">{{ item.icon }}</span> <AppIcon class="bottom-nav-icon" :icon="item.icon" :size="20" />
<span class="bottom-nav-label">{{ item.label }}</span> <span class="bottom-nav-label">{{ item.label }}</span>
</RouterLink> </RouterLink>
</nav> </nav>
@@ -78,7 +79,7 @@ function isActive(to: string) {
transition: color 0.2s; transition: color 0.2s;
} }
.bottom-nav-item.active { color: var(--color-primary-dark); } .bottom-nav-item.active { color: var(--color-primary-dark); }
.bottom-nav-icon { font-size: 20px; } .bottom-nav-icon { color: currentColor; }
.bottom-nav-label { font-size: 10px; font-weight: 600; } .bottom-nav-label { font-size: 10px; font-weight: 600; }
@media (max-width: 768px) { .bottom-nav { display: flex; } } @media (max-width: 768px) { .bottom-nav { display: flex; } }
+24 -21
View File
@@ -2,6 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import AppIcon from '@/components/ui/AppIcon.vue'
const auth = useAuthStore() const auth = useAuthStore()
const router = useRouter() const router = useRouter()
@@ -10,22 +11,22 @@ const route = useRoute()
interface NavItem { label: string; icon: string; to: string; roles: string[] } interface NavItem { label: string; icon: string; to: string; roles: string[] }
const navItems: NavItem[] = [ const navItems: NavItem[] = [
{ label: 'Главная', icon: '🏠', to: '/', roles: ['student'] }, { label: 'Главная', icon: 'home', to: '/', roles: ['student'] },
{ label: 'Каталог', icon: '📚', to: '/catalog', roles: ['student'] }, { label: 'Каталог', icon: 'books', to: '/catalog', roles: ['student'] },
{ label: 'Мои лекции', icon: '📋', to: '/my-lectures', roles: ['student'] }, { label: 'Мои лекции', icon: 'clipboard-list', to: '/my-lectures', roles: ['student'] },
{ label: 'Достижения', icon: '🏆', to: '/achievements', roles: ['student'] }, { label: 'Достижения', icon: 'trophy', to: '/achievements', roles: ['student'] },
{ label: 'Уведомления', icon: '🔔', to: '/notifications', roles: ['student'] }, { label: 'Уведомления', icon: 'bell', to: '/notifications', roles: ['student'] },
{ label: 'Профиль', icon: '👤', to: '/profile', roles: ['student'] }, { label: 'Профиль', icon: 'user', to: '/profile', roles: ['student'] },
// Teacher // Teacher
{ label: 'Дашборд', icon: '📊', to: '/teacher', roles: ['teacher'] }, { label: 'Дашборд', icon: 'chart-bar', to: '/teacher', roles: ['teacher'] },
{ label: 'Лекции', icon: '📖', to: '/teacher/lectures',roles: ['teacher'] }, { label: 'Лекции', icon: 'book-2', to: '/teacher/lectures', roles: ['teacher'] },
{ label: 'Аналитика', icon: '📈', to: '/teacher/analytics',roles: ['teacher'] }, { label: 'Аналитика', icon: 'chart-line', to: '/teacher/analytics', roles: ['teacher'] },
{ label: 'Профиль', icon: '👤', to: '/profile', roles: ['teacher'] }, { label: 'Профиль', icon: 'user', to: '/profile', roles: ['teacher'] },
// Admin // Admin
{ label: 'Дашборд', icon: '🛡️', to: '/admin', roles: ['admin'] }, { label: 'Дашборд', icon: 'shield', to: '/admin', roles: ['admin'] },
{ label: 'Пользователи',icon: '👥', to: '/admin/users', roles: ['admin'] }, { label: 'Пользователи', icon: 'users', to: '/admin/users', roles: ['admin'] },
{ label: 'Лекции', icon: '📚', to: '/admin/lectures', roles: ['admin'] }, { label: 'Лекции', icon: 'books', to: '/admin/lectures', roles: ['admin'] },
{ label: 'ИИ очередь', icon: '🤖', to: '/admin/llm-queue', roles: ['admin'] }, { label: 'ИИ очередь', icon: 'robot', to: '/admin/llm-queue', roles: ['admin'] },
] ]
const visible = computed(() => const visible = computed(() =>
@@ -44,18 +45,19 @@ function isActive(to: string) {
<RouterLink <RouterLink
v-for="item in visible" v-for="item in visible"
:key="item.to + item.label" :key="item.to + item.label"
:to="item.to" :to="item.to"
class="nav-item" class="nav-item"
:class="{ active: isActive(item.to) }" :class="{ active: isActive(item.to) }"
> >
<span class="nav-icon">{{ item.icon }}</span> <AppIcon class="nav-icon" :icon="item.icon" :size="17" />
<span class="nav-label">{{ item.label }}</span> <span class="nav-label">{{ item.label }}</span>
</RouterLink> </RouterLink>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
<button class="logout-btn" @click="auth.logout().then(() => router.push('/login'))"> <button class="logout-btn" @click="auth.logout().then(() => router.push('/login'))">
🚪 Выйти <AppIcon class="logout-icon" icon="logout" :size="16" />
Выйти
</button> </button>
</div> </div>
</aside> </aside>
@@ -107,7 +109,7 @@ function isActive(to: string) {
font-weight: 700; font-weight: 700;
box-shadow: 0 2px 8px rgba(34,197,94,0.12); box-shadow: 0 2px 8px rgba(34,197,94,0.12);
} }
.nav-icon { font-size: 17px; flex-shrink: 0; } .nav-icon { flex-shrink: 0; color: currentColor; }
.sidebar-footer { padding: 10px 18px 8px; } .sidebar-footer { padding: 10px 18px 8px; }
.logout-btn { .logout-btn {
width: 100%; width: 100%;
@@ -124,6 +126,7 @@ function isActive(to: string) {
align-items: center; align-items: center;
gap: 6px; gap: 6px;
} }
.logout-icon { color: currentColor; }
.logout-btn:hover { background: rgba(239,68,68,0.15); } .logout-btn:hover { background: rgba(239,68,68,0.15); }
@media (max-width: 768px) { .sidebar { display: none; } } @media (max-width: 768px) { .sidebar { display: none; } }
+6 -5
View File
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import CoinChip from '@/components/ui/CoinChip.vue' import CoinChip from '@/components/ui/CoinChip.vue'
import SearchInput from '@/components/ui/SearchInput.vue' import SearchInput from '@/components/ui/SearchInput.vue'
import AppIcon from '@/components/ui/AppIcon.vue'
import { formatUserName } from '@/utils/formatUserName' import { formatUserName } from '@/utils/formatUserName'
const auth = useAuthStore() const auth = useAuthStore()
@@ -35,7 +36,7 @@ function openProfile() {
<template> <template>
<header class="topbar"> <header class="topbar">
<div class="topbar-brand"> <div class="topbar-brand">
<span class="brand-icon">🌍</span> <AppIcon class="brand-icon" icon="world" :size="24" />
<span class="brand-name">UniVerse</span> <span class="brand-name">UniVerse</span>
</div> </div>
@@ -51,7 +52,7 @@ function openProfile() {
</button> </button>
<button class="notif-btn" @click="$router.push('/notifications')"> <button class="notif-btn" @click="$router.push('/notifications')">
🔔 <AppIcon icon="bell" :size="18" />
<span class="notif-dot" v-if="auth.user && unreadCount > 0"> <span class="notif-dot" v-if="auth.user && unreadCount > 0">
{{ unreadCount }} {{ unreadCount }}
</span> </span>
@@ -65,7 +66,7 @@ function openProfile() {
@keydown.enter.prevent="openProfile" @keydown.enter.prevent="openProfile"
@keydown.space.prevent="openProfile" @keydown.space.prevent="openProfile"
> >
<span class="avatar-icon">👤</span> <AppIcon class="avatar-icon" icon="user" :size="18" />
<span class="avatar-name" v-if="auth.user">{{ formatUserName(auth.user.name) }}</span> <span class="avatar-name" v-if="auth.user">{{ formatUserName(auth.user.name) }}</span>
</div> </div>
</div> </div>
@@ -98,7 +99,7 @@ function openProfile() {
flex-shrink: 0; flex-shrink: 0;
cursor: pointer; cursor: pointer;
} }
.brand-icon { font-size: 24px; } .brand-icon { color: var(--color-text); }
.brand-name { .brand-name {
font-size: 20px; font-size: 20px;
font-weight: 800; font-weight: 800;
@@ -165,7 +166,7 @@ function openProfile() {
transition: all 0.2s; transition: all 0.2s;
} }
.avatar:hover { background: rgba(255,255,255,0.8); } .avatar:hover { background: rgba(255,255,255,0.8); }
.avatar-icon { font-size: 18px; } .avatar-icon { color: var(--color-text-secondary); }
.avatar-name { font-size: 13px; font-weight: 600; color: var(--color-text); } .avatar-name { font-size: 13px; font-weight: 600; color: var(--color-text); }
@media (max-width: 640px) { @media (max-width: 640px) {
@@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import AppIcon from '@/components/ui/AppIcon.vue'
defineProps<{ defineProps<{
icon: string icon: string
title: string title: string
@@ -11,15 +13,22 @@ defineProps<{
<template> <template>
<div class="badge-card" :class="{ locked: !unlocked }"> <div class="badge-card" :class="{ locked: !unlocked }">
<div class="badge-icon">{{ icon }}</div> <AppIcon class="badge-icon" :icon="icon" :size="32" />
<div class="badge-body"> <div class="badge-body">
<div class="badge-title">{{ title }}</div> <div class="badge-title">{{ title }}</div>
<div class="badge-desc">{{ description }}</div> <div class="badge-desc">{{ description }}</div>
<div class="badge-meta" v-if="unlocked && unlockedAt"> <div class="badge-meta" v-if="unlocked && unlockedAt">
Получено {{ new Date(unlockedAt).toLocaleDateString('ru-RU') }} <AppIcon class="meta-icon" icon="circle-check" :size="14" />
<span v-if="coins" class="coins-tag">+{{ coins }} 💰</span> Получено {{ new Date(unlockedAt).toLocaleDateString('ru-RU') }}
<span v-if="coins" class="coins-tag">
+{{ coins }}
<AppIcon class="coin-icon" icon="coin" :size="14" />
</span>
</div>
<div class="badge-meta locked-msg" v-else-if="!unlocked">
<AppIcon class="meta-icon" icon="lock" :size="14" />
Заблокировано
</div> </div>
<div class="badge-meta locked-msg" v-else-if="!unlocked">🔒 Заблокировано</div>
</div> </div>
</div> </div>
</template> </template>
@@ -42,10 +51,11 @@ defineProps<{
opacity: 0.5; opacity: 0.5;
filter: grayscale(0.5); filter: grayscale(0.5);
} }
.badge-icon { font-size: 32px; line-height: 1; flex-shrink: 0; } .badge-icon { flex-shrink: 0; color: var(--color-text); }
.badge-title { font-size: 15px; font-weight: 700; color: var(--color-text); margin-bottom: 3px; } .badge-title { font-size: 15px; font-weight: 700; color: var(--color-text); margin-bottom: 3px; }
.badge-desc { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 6px; } .badge-desc { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 6px; }
.badge-meta { font-size: 11px; color: var(--color-text-secondary); } .badge-meta { font-size: 11px; color: var(--color-text-secondary); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.meta-icon { color: var(--color-text-secondary); }
.locked-msg { color: #9CA3AF; } .locked-msg { color: #9CA3AF; }
.coins-tag { .coins-tag {
margin-left: 6px; margin-left: 6px;
@@ -54,5 +64,9 @@ defineProps<{
padding: 1px 8px; padding: 1px 8px;
color: #92400E; color: #92400E;
font-weight: 600; font-weight: 600;
display: inline-flex;
align-items: center;
gap: 4px;
} }
.coin-icon { color: #92400E; }
</style> </style>
+68
View File
@@ -0,0 +1,68 @@
<script setup lang="ts">
import { computed } from 'vue'
import { iconSvgs, normalizeIconName } from '@/icons'
const props = defineProps<{
icon?: string | null
size?: number | string
title?: string
}>()
const isUrl = computed(() => {
const value = props.icon ?? ''
return /^(https?:\/\/|\/|data:)/.test(value)
})
const svg = computed(() => {
const name = normalizeIconName(props.icon)
if (!name) return undefined
return iconSvgs[name]
})
const sizePx = computed(() => {
const size = props.size ?? 18
return typeof size === 'number' ? `${size}px` : size
})
</script>
<template>
<span
v-if="svg"
class="app-icon"
:style="{ '--size': sizePx }"
:aria-hidden="title ? undefined : 'true'"
:aria-label="title"
role="img"
v-html="svg"
/>
<img
v-else-if="isUrl && icon"
class="app-icon-img"
:src="icon"
:alt="title ?? ''"
:style="{ width: sizePx, height: sizePx }"
loading="lazy"
decoding="async"
/>
</template>
<style scoped>
.app-icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.app-icon :deep(svg) {
display: block;
width: var(--size);
height: var(--size);
}
.app-icon-img {
display: inline-block;
vertical-align: middle;
}
</style>
+4 -2
View File
@@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import AppIcon from '@/components/ui/AppIcon.vue'
defineProps<{ amount: number }>() defineProps<{ amount: number }>()
</script> </script>
<template> <template>
<div class="coin-chip"> <div class="coin-chip">
<span class="coin-icon">💰</span> <AppIcon class="coin-icon" icon="coin" :size="16" />
<span class="coin-amount">{{ amount }}</span> <span class="coin-amount">{{ amount }}</span>
<span class="coin-label">монет</span> <span class="coin-label">монет</span>
</div> </div>
@@ -26,7 +28,7 @@ defineProps<{ amount: number }>()
.coin-chip:hover { .coin-chip:hover {
background: linear-gradient(135deg, rgba(251,191,36,0.3), rgba(245,158,11,0.25)); background: linear-gradient(135deg, rgba(251,191,36,0.3), rgba(245,158,11,0.25));
} }
.coin-icon { font-size: 16px; } .coin-icon { color: #78350F; }
.coin-amount { font-weight: 800; font-size: 14px; color: #78350F; } .coin-amount { font-weight: 800; font-size: 14px; color: #78350F; }
.coin-label { font-size: 12px; color: #92400E; } .coin-label { font-size: 12px; color: #92400E; }
</style> </style>
+4 -2
View File
@@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import AppIcon from '@/components/ui/AppIcon.vue'
defineProps<{ defineProps<{
icon?: string icon?: string
title?: string title?: string
@@ -8,7 +10,7 @@ defineProps<{
<template> <template>
<div class="empty-state"> <div class="empty-state">
<div class="empty-icon">{{ icon ?? '📭' }}</div> <AppIcon class="empty-icon" :icon="icon ?? 'inbox'" :size="52" />
<div class="empty-title">{{ title ?? 'Ничего не найдено' }}</div> <div class="empty-title">{{ title ?? 'Ничего не найдено' }}</div>
<div class="empty-sub">{{ subtitle ?? 'Попробуйте изменить фильтры или вернитесь позже.' }}</div> <div class="empty-sub">{{ subtitle ?? 'Попробуйте изменить фильтры или вернитесь позже.' }}</div>
<slot /> <slot />
@@ -26,7 +28,7 @@ defineProps<{
text-align: center; text-align: center;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.empty-icon { font-size: 52px; } .empty-icon { color: var(--color-text-secondary); }
.empty-title { font-size: 18px; font-weight: 700; color: var(--color-text); } .empty-title { font-size: 18px; font-weight: 700; color: var(--color-text); }
.empty-sub { font-size: 14px; max-width: 320px; } .empty-sub { font-size: 14px; max-width: 320px; }
</style> </style>
+25 -6
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Lecture } from '@/types' import type { Lecture } from '@/types'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import AppIcon from '@/components/ui/AppIcon.vue'
const props = defineProps<{ const props = defineProps<{
lecture: Lecture lecture: Lecture
@@ -28,7 +29,8 @@ function goDetail() {
<div class="lecture-card" @click="goDetail"> <div class="lecture-card" @click="goDetail">
<div class="card-header"> <div class="card-header">
<span class="badge" :class="lecture.format === 'online' ? 'badge-blue' : 'badge-green'"> <span class="badge" :class="lecture.format === 'online' ? 'badge-blue' : 'badge-green'">
{{ lecture.format === 'online' ? '🌐 Онлайн' : '📍 Офлайн' }} <AppIcon class="badge-icon" :icon="lecture.format === 'online' ? 'world' : 'map-pin'" :size="14" />
{{ lecture.format === 'online' ? 'Онлайн' : 'Офлайн' }}
</span> </span>
<span <span
class="seats" class="seats"
@@ -47,16 +49,28 @@ function goDetail() {
<h3 class="card-title">{{ lecture.title }}</h3> <h3 class="card-title">{{ lecture.title }}</h3>
<div class="card-teacher"> <div class="card-teacher">
<span>👤</span> <AppIcon class="teacher-icon" icon="user" :size="16" />
<span>{{ lecture.teacher }}</span> <span>{{ lecture.teacher }}</span>
<span class="text-secondary">· {{ lecture.institute }}</span> <span class="text-secondary">· {{ lecture.institute }}</span>
</div> </div>
<div class="card-meta"> <div class="card-meta">
<span>📅 {{ formatDate(lecture.date) }}</span> <span class="meta-item">
<span> {{ lecture.time }}</span> <AppIcon class="meta-icon" icon="calendar" :size="14" />
<span v-if="lecture.room">🏛 {{ lecture.building }}, ауд. {{ lecture.room }}</span> {{ formatDate(lecture.date) }}
<span v-else>🏛 {{ lecture.building }}</span> </span>
<span class="meta-item">
<AppIcon class="meta-icon" icon="clock" :size="14" />
{{ lecture.time }}
</span>
<span v-if="lecture.room" class="meta-item">
<AppIcon class="meta-icon" icon="building" :size="14" />
{{ lecture.building }}, ауд. {{ lecture.room }}
</span>
<span v-else class="meta-item">
<AppIcon class="meta-icon" icon="building" :size="14" />
{{ lecture.building }}
</span>
</div> </div>
<div class="card-tags"> <div class="card-tags">
@@ -117,6 +131,8 @@ function goDetail() {
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
} }
.badge { display: inline-flex; align-items: center; gap: 4px; }
.badge-icon { color: currentColor; }
.seats { .seats {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
@@ -141,6 +157,7 @@ function goDetail() {
font-size: 13px; font-size: 13px;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.teacher-icon { color: var(--color-text-secondary); }
.card-meta { .card-meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -148,6 +165,8 @@ function goDetail() {
font-size: 12px; font-size: 12px;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.meta-item { display: inline-flex; align-items: center; gap: 4px; }
.meta-icon { color: var(--color-text-secondary); }
.card-tags { .card-tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
+4 -2
View File
@@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import AppIcon from '@/components/ui/AppIcon.vue'
const props = defineProps<{ const props = defineProps<{
modelValue: string modelValue: string
placeholder?: string placeholder?: string
@@ -8,7 +10,7 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
<template> <template>
<div class="search-wrap"> <div class="search-wrap">
<span class="search-icon">🔍</span> <AppIcon class="search-icon" icon="search" :size="15" />
<input <input
class="search-input" class="search-input"
type="text" type="text"
@@ -28,7 +30,7 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
.search-icon { .search-icon {
position: absolute; position: absolute;
left: 12px; left: 12px;
font-size: 15px; color: var(--color-text-secondary);
pointer-events: none; pointer-events: none;
} }
.search-input { .search-input {
+4 -3
View File
@@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import AppIcon from '@/components/ui/AppIcon.vue'
defineProps<{ defineProps<{
label: string label: string
value: string | number value: string | number
@@ -10,7 +12,7 @@ defineProps<{
<template> <template>
<div class="stats-widget" :class="`color-${color ?? 'green'}`"> <div class="stats-widget" :class="`color-${color ?? 'green'}`">
<div class="widget-icon" v-if="icon">{{ icon }}</div> <AppIcon v-if="icon" class="widget-icon" :icon="icon" :size="28" />
<div class="widget-body"> <div class="widget-body">
<div class="widget-value">{{ value }}</div> <div class="widget-value">{{ value }}</div>
<div class="widget-label">{{ label }}</div> <div class="widget-label">{{ label }}</div>
@@ -47,9 +49,8 @@ defineProps<{
.color-purple::after { background: linear-gradient(90deg, #A78BFA, #C4B5FD); } .color-purple::after { background: linear-gradient(90deg, #A78BFA, #C4B5FD); }
.widget-icon { .widget-icon {
font-size: 28px;
line-height: 1;
flex-shrink: 0; flex-shrink: 0;
color: var(--color-text);
} }
.widget-body { flex: 1; min-width: 0; } .widget-body { flex: 1; min-width: 0; }
.widget-value { .widget-value {
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import AppIcon from '@/components/ui/AppIcon.vue'
const props = defineProps<{ const props = defineProps<{
message: string message: string
@@ -14,13 +15,13 @@ onMounted(() => {
setTimeout(() => { visible.value = false; setTimeout(() => emit('close'), 350) }, props.duration ?? 3000) setTimeout(() => { visible.value = false; setTimeout(() => emit('close'), 350) }, props.duration ?? 3000)
}) })
const iconMap = { success: '', error: '', info: '' } const iconNameMap = { success: 'circle-check', error: 'circle-x', info: 'info-circle' } as const
</script> </script>
<template> <template>
<Transition name="toast"> <Transition name="toast">
<div v-if="visible" class="toast" :class="type ?? 'success'"> <div v-if="visible" class="toast" :class="type ?? 'success'">
<span>{{ iconMap[type ?? 'success'] }}</span> <AppIcon :icon="iconNameMap[type ?? 'success']" :size="18" />
<span>{{ message }}</span> <span>{{ message }}</span>
<button class="toast-close" @click="emit('close')">×</button> <button class="toast-close" @click="emit('close')">×</button>
</div> </div>
+127
View File
@@ -0,0 +1,127 @@
import alarm from '@/assets/icons/alarm.svg?raw'
import alertTriangle from '@/assets/icons/alert-triangle.svg?raw'
import bell from '@/assets/icons/bell.svg?raw'
import book2 from '@/assets/icons/book-2.svg?raw'
import books from '@/assets/icons/books.svg?raw'
import building from '@/assets/icons/building.svg?raw'
import bulb from '@/assets/icons/bulb.svg?raw'
import calendar from '@/assets/icons/calendar.svg?raw'
import calendarEvent from '@/assets/icons/calendar-event.svg?raw'
import chartBar from '@/assets/icons/chart-bar.svg?raw'
import chartLine from '@/assets/icons/chart-line.svg?raw'
import circleCheck from '@/assets/icons/circle-check.svg?raw'
import circleX from '@/assets/icons/circle-x.svg?raw'
import clipboardList from '@/assets/icons/clipboard-list.svg?raw'
import clock from '@/assets/icons/clock.svg?raw'
import coin from '@/assets/icons/coin.svg?raw'
import handStop from '@/assets/icons/hand-stop.svg?raw'
import home from '@/assets/icons/home.svg?raw'
import inbox from '@/assets/icons/inbox.svg?raw'
import infoCircle from '@/assets/icons/info-circle.svg?raw'
import lock from '@/assets/icons/lock.svg?raw'
import logout from '@/assets/icons/logout.svg?raw'
import mapPin from '@/assets/icons/map-pin.svg?raw'
import messageCircle from '@/assets/icons/message-circle.svg?raw'
import moodNeutral from '@/assets/icons/mood-neutral.svg?raw'
import robot from '@/assets/icons/robot.svg?raw'
import search from '@/assets/icons/search.svg?raw'
import shield from '@/assets/icons/shield.svg?raw'
import sparkles from '@/assets/icons/sparkles.svg?raw'
import star from '@/assets/icons/star.svg?raw'
import stopwatch from '@/assets/icons/stopwatch.svg?raw'
import thumbDown from '@/assets/icons/thumb-down.svg?raw'
import thumbUp from '@/assets/icons/thumb-up.svg?raw'
import trophy from '@/assets/icons/trophy.svg?raw'
import user from '@/assets/icons/user.svg?raw'
import users from '@/assets/icons/users.svg?raw'
import world from '@/assets/icons/world.svg?raw'
export const iconSvgs = {
alarm,
'alert-triangle': alertTriangle,
bell,
'book-2': book2,
books,
building,
bulb,
calendar,
'calendar-event': calendarEvent,
'chart-bar': chartBar,
'chart-line': chartLine,
'circle-check': circleCheck,
'circle-x': circleX,
'clipboard-list': clipboardList,
clock,
coin,
'hand-stop': handStop,
home,
inbox,
'info-circle': infoCircle,
lock,
logout,
'map-pin': mapPin,
'message-circle': messageCircle,
'mood-neutral': moodNeutral,
robot,
search,
shield,
sparkles,
star,
stopwatch,
'thumb-down': thumbDown,
'thumb-up': thumbUp,
trophy,
user,
users,
world,
} as const
export type IconName = keyof typeof iconSvgs
export const emojiToIcon: Record<string, IconName> = {
'⏰': 'alarm',
'⚠️': 'alert-triangle',
'🔔': 'bell',
'📖': 'book-2',
'📚': 'books',
'🏛': 'building',
'💡': 'bulb',
'📅': 'calendar',
'🗓️': 'calendar-event',
'📊': 'chart-bar',
'📈': 'chart-line',
'✅': 'circle-check',
'❌': 'circle-x',
'📋': 'clipboard-list',
'⏱': 'stopwatch',
'⏱️': 'stopwatch',
'🕒': 'clock',
'💰': 'coin',
'👋': 'hand-stop',
'🏠': 'home',
'📭': 'inbox',
'️': 'info-circle',
'🔒': 'lock',
'🚪': 'logout',
'📍': 'map-pin',
'💬': 'message-circle',
'😐': 'mood-neutral',
'🤖': 'robot',
'🔍': 'search',
'🛡️': 'shield',
'✨': 'sparkles',
'⭐': 'star',
'👍': 'thumb-up',
'👎': 'thumb-down',
'🏆': 'trophy',
'👤': 'user',
'👥': 'users',
'🌍': 'world',
'🌐': 'world',
} as const
export function normalizeIconName(input?: string | null): IconName | undefined {
if (!input) return undefined
if (input in iconSvgs) return input as IconName
return emojiToIcon[input]
}
@@ -44,10 +44,10 @@ onMounted(async () => {
<h1 class="page-title">Дашборд администратора</h1> <h1 class="page-title">Дашборд администратора</h1>
<div class="stats-row"> <div class="stats-row">
<StatsWidget label="Пользователей" :value="users.length" icon="👥" color="green" /> <StatsWidget label="Пользователей" :value="users.length" icon="users" color="green" />
<StatsWidget label="Лекций" :value="lectures.length" icon="📚" color="aqua" /> <StatsWidget label="Лекций" :value="lectures.length" icon="books" color="aqua" />
<StatsWidget label="Записей" :value="enrollmentCount" icon="🗓️" color="orange" /> <StatsWidget label="Записей" :value="enrollmentCount" icon="calendar-event" color="orange" />
<StatsWidget label="Отзывов в LLM" :value="reviews.length" icon="💬" color="purple" /> <StatsWidget label="Отзывов в LLM" :value="reviews.length" icon="message-circle" color="purple" />
</div> </div>
<div class="grid"> <div class="grid">
+13 -3
View File
@@ -2,6 +2,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import AppIcon from '@/components/ui/AppIcon.vue'
const auth = useAuthStore() const auth = useAuthStore()
const route = useRoute() const route = useRoute()
@@ -23,7 +24,9 @@ async function login() {
<div class="login-bg"> <div class="login-bg">
<div class="login-card"> <div class="login-card">
<div class="login-header"> <div class="login-header">
<div class="logo-mark">🌍</div> <div class="logo-mark">
<AppIcon icon="world" :size="52" />
</div>
<h1 class="brand">UniVerse</h1> <h1 class="brand">UniVerse</h1>
<p class="brand-sub">«Откройте для себя вселенную знаний»</p> <p class="brand-sub">«Откройте для себя вселенную знаний»</p>
</div> </div>
@@ -40,7 +43,10 @@ async function login() {
</span> </span>
{{ loading ? 'Вход...' : 'Войти через ЮФУ (Microsoft Entra ID)' }} {{ loading ? 'Вход...' : 'Войти через ЮФУ (Microsoft Entra ID)' }}
</button> </button>
<div class="error" v-if="error"> {{ error }}</div> <div class="error" v-if="error">
<AppIcon class="error-icon" icon="alert-triangle" :size="16" />
{{ error }}
</div>
</div> </div>
<div class="login-footer"> <div class="login-footer">
@@ -73,7 +79,7 @@ async function login() {
gap: 20px; gap: 20px;
} }
.login-header { text-align: center; } .login-header { text-align: center; }
.logo-mark { font-size: 52px; } .logo-mark { display: inline-flex; justify-content: center; color: var(--color-text); }
.brand { .brand {
font-size: 34px; font-size: 34px;
font-weight: 900; font-weight: 900;
@@ -102,7 +108,11 @@ async function login() {
border: 1px solid rgba(239,68,68,0.3); border: 1px solid rgba(239,68,68,0.3);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
padding: 8px 12px; padding: 8px 12px;
display: flex;
align-items: center;
gap: 8px;
} }
.error-icon { color: var(--color-error); }
.login-footer { .login-footer {
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
+44 -11
View File
@@ -10,6 +10,7 @@ import LectureCard from '@/components/ui/LectureCard.vue'
import ProgressBar from '@/components/ui/ProgressBar.vue' import ProgressBar from '@/components/ui/ProgressBar.vue'
import AchievementBadge from '@/components/ui/AchievementBadge.vue' import AchievementBadge from '@/components/ui/AchievementBadge.vue'
import EmptyState from '@/components/ui/EmptyState.vue' import EmptyState from '@/components/ui/EmptyState.vue'
import AppIcon from '@/components/ui/AppIcon.vue'
import { formatUserName } from '@/utils/formatUserName' import { formatUserName } from '@/utils/formatUserName'
const auth = useAuthStore() const auth = useAuthStore()
@@ -40,7 +41,10 @@ onMounted(async () => {
<div class="dashboard page-content"> <div class="dashboard page-content">
<div class="dashboard-welcome"> <div class="dashboard-welcome">
<div> <div>
<h1 class="page-title">Добрый день, {{ formatUserName(user.name) }}! 👋</h1> <h1 class="page-title">
Добрый день, {{ formatUserName(user.name) }}!
<AppIcon class="inline-icon" icon="hand-stop" :size="18" />
</h1>
<p class="text-secondary">{{ user.institute }} · {{ user.direction }} · {{ user.year }} курс</p> <p class="text-secondary">{{ user.institute }} · {{ user.direction }} · {{ user.year }} курс</p>
</div> </div>
<div class="quick-actions"> <div class="quick-actions">
@@ -56,9 +60,18 @@ onMounted(async () => {
<div class="section-title">Ближайшая лекция</div> <div class="section-title">Ближайшая лекция</div>
<div class="next-title">{{ nextLecture.title }}</div> <div class="next-title">{{ nextLecture.title }}</div>
<div class="next-meta"> <div class="next-meta">
<span>📅 Завтра, {{ nextLecture.time }}</span> <span class="meta-line">
<span>🏛 {{ nextLecture.building }}, ауд. {{ nextLecture.room ?? 'онлайн' }}</span> <AppIcon class="meta-icon" icon="calendar" :size="14" />
<span>👤 {{ nextLecture.teacher }}</span> Завтра, {{ nextLecture.time }}
</span>
<span class="meta-line">
<AppIcon class="meta-icon" icon="building" :size="14" />
{{ nextLecture.building }}, ауд. {{ nextLecture.room ?? 'онлайн' }}
</span>
<span class="meta-line">
<AppIcon class="meta-icon" icon="user" :size="14" />
{{ nextLecture.teacher }}
</span>
</div> </div>
</div> </div>
<div class="next-actions"> <div class="next-actions">
@@ -70,10 +83,10 @@ onMounted(async () => {
<EmptyState v-else-if="!lectures.loading" title="Пока нет лекций" subtitle="Каталог пуст или данные ещё не синхронизированы." /> <EmptyState v-else-if="!lectures.loading" title="Пока нет лекций" subtitle="Каталог пуст или данные ещё не синхронизированы." />
<div class="stats-row"> <div class="stats-row">
<StatsWidget label="Посещено лекций" :value="user.lecturesAttended ?? 12" icon="📚" color="green" /> <StatsWidget label="Посещено лекций" :value="user.lecturesAttended ?? 12" icon="books" color="green" />
<StatsWidget label="Часов обучения" :value="user.hoursLearned ?? 18.5" icon="" color="aqua" /> <StatsWidget label="Часов обучения" :value="user.hoursLearned ?? 18.5" icon="stopwatch" color="aqua" />
<StatsWidget label="Монет" :value="user.coins" icon="💰" color="orange" /> <StatsWidget label="Монет" :value="user.coins" icon="coin" color="orange" />
<StatsWidget label="Уровень" :value="user.level" icon="" color="purple" sub="текущий уровень" /> <StatsWidget label="Уровень" :value="user.level" icon="star" color="purple" sub="текущий уровень" />
</div> </div>
<GlassCard> <GlassCard>
@@ -88,7 +101,12 @@ onMounted(async () => {
<section> <section>
<div class="section-header"> <div class="section-header">
<h2 class="section-title"> Рекомендуемые лекции</h2> <h2 class="section-title">
<span class="title-with-icon">
<AppIcon class="title-icon" icon="sparkles" :size="18" />
Рекомендуемые лекции
</span>
</h2>
<button class="link-btn" @click="router.push('/catalog')">Все лекции </button> <button class="link-btn" @click="router.push('/catalog')">Все лекции </button>
</div> </div>
<EmptyState v-if="lectures.loading" title="Загружаем рекомендации" subtitle="Получаем данные с backend." /> <EmptyState v-if="lectures.loading" title="Загружаем рекомендации" subtitle="Получаем данные с backend." />
@@ -105,7 +123,12 @@ onMounted(async () => {
<section class="two-column"> <section class="two-column">
<GlassCard> <GlassCard>
<div class="section-title">🏆 Достижения</div> <div class="section-title">
<span class="title-with-icon">
<AppIcon class="title-icon" icon="trophy" :size="18" />
Достижения
</span>
</div>
<div class="achievements"> <div class="achievements">
<AchievementBadge <AchievementBadge
v-for="a in achievements" v-for="a in achievements"
@@ -120,7 +143,12 @@ onMounted(async () => {
</div> </div>
</GlassCard> </GlassCard>
<GlassCard> <GlassCard>
<div class="section-title">🔔 Напоминания</div> <div class="section-title">
<span class="title-with-icon">
<AppIcon class="title-icon" icon="bell" :size="18" />
Напоминания
</span>
</div>
<div class="reminders"> <div class="reminders">
<div class="reminder-item" v-for="n in reminders" :key="n.id"> <div class="reminder-item" v-for="n in reminders" :key="n.id">
<div class="reminder-title">{{ n.title }}</div> <div class="reminder-title">{{ n.title }}</div>
@@ -146,6 +174,8 @@ onMounted(async () => {
.next-lecture { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; } .next-lecture { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.next-title { font-size: 18px; font-weight: 700; margin: 6px 0; } .next-title { font-size: 18px; font-weight: 700; margin: 6px 0; }
.next-meta { display: flex; flex-direction: column; gap: 4px; color: var(--color-text-secondary); font-size: 13px; } .next-meta { display: flex; flex-direction: column; gap: 4px; color: var(--color-text-secondary); font-size: 13px; }
.meta-line { display: inline-flex; align-items: center; gap: 6px; }
.meta-icon { color: var(--color-text-secondary); }
.next-actions { display: flex; gap: 10px; align-items: flex-start; } .next-actions { display: flex; gap: 10px; align-items: flex-start; }
.stats-row { .stats-row {
display: grid; display: grid;
@@ -156,6 +186,9 @@ onMounted(async () => {
.xp-header { display: flex; justify-content: space-between; font-size: 13px; font-weight: 600; } .xp-header { display: flex; justify-content: space-between; font-size: 13px; font-weight: 600; }
.xp-val { color: var(--color-text-secondary); } .xp-val { color: var(--color-text-secondary); }
.section-header { display: flex; align-items: center; justify-content: space-between; } .section-header { display: flex; align-items: center; justify-content: space-between; }
.title-with-icon { display: inline-flex; align-items: center; gap: 8px; }
.title-icon { color: var(--color-text); }
.inline-icon { color: var(--color-text); vertical-align: middle; }
.cards-grid { .cards-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
@@ -2,6 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import GlassCard from '@/components/ui/GlassCard.vue' import GlassCard from '@/components/ui/GlassCard.vue'
import AppIcon from '@/components/ui/AppIcon.vue'
const userStore = useUserStore() const userStore = useUserStore()
@@ -16,11 +17,11 @@ const grouped = computed(() => {
}) })
const typeIcon: Record<string, string> = { const typeIcon: Record<string, string> = {
reminder: '', reminder: 'alarm',
'schedule-change': '🗓️', 'schedule-change': 'calendar-event',
achievement: '🏆', achievement: 'trophy',
coins: '💰', coins: 'coin',
recommendation: '', recommendation: 'sparkles',
} }
</script> </script>
@@ -36,7 +37,7 @@ const typeIcon: Record<string, string> = {
<div class="group-title">{{ day }}</div> <div class="group-title">{{ day }}</div>
<div class="items"> <div class="items">
<div v-for="n in items" :key="n.id" class="item" :class="{ unread: !n.read }"> <div v-for="n in items" :key="n.id" class="item" :class="{ unread: !n.read }">
<div class="icon">{{ typeIcon[n.type] }}</div> <AppIcon class="icon" :icon="typeIcon[n.type]" :size="20" />
<div> <div>
<div class="item-title">{{ n.title }}</div> <div class="item-title">{{ n.title }}</div>
<div class="item-body">{{ n.body }}</div> <div class="item-body">{{ n.body }}</div>
@@ -56,7 +57,7 @@ const typeIcon: Record<string, string> = {
.items { display: flex; flex-direction: column; gap: 10px; } .items { display: flex; flex-direction: column; gap: 10px; }
.item { display: flex; gap: 12px; padding: 10px; border-radius: var(--radius-sm); background: rgba(255,255,255,0.6); border: 1px solid var(--color-border-glass); } .item { display: flex; gap: 12px; padding: 10px; border-radius: var(--radius-sm); background: rgba(255,255,255,0.6); border: 1px solid var(--color-border-glass); }
.item.unread { border-color: rgba(34,197,94,0.4); background: rgba(34,197,94,0.08); } .item.unread { border-color: rgba(34,197,94,0.4); background: rgba(34,197,94,0.08); }
.icon { font-size: 20px; } .icon { color: var(--color-text); flex-shrink: 0; }
.item-title { font-weight: 600; } .item-title { font-weight: 600; }
.item-body { font-size: 13px; color: var(--color-text-secondary); } .item-body { font-size: 13px; color: var(--color-text-secondary); }
</style> </style>