@heyramzi/ui 2.0.1 → 2.1.0

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 (68) hide show
  1. package/package.json +42 -17
  2. package/src/canvas/README.md +9 -12
  3. package/src/charts/dashboard/data-panel.svelte +1 -1
  4. package/src/components/list-page/detail-panel.svelte +1 -1
  5. package/src/components/list-page/filter-popover.svelte +1 -1
  6. package/src/components/list-page/index.ts +3 -3
  7. package/src/components/list-page/rail-head.svelte +1 -1
  8. package/src/components/section-header/index.ts +1 -0
  9. package/src/components/section-header/section-header.svelte +1 -1
  10. package/src/components/toolbar/index.ts +2 -0
  11. package/src/components/toolbar/toolbar.svelte +2 -2
  12. package/src/inbox/README.md +29 -0
  13. package/src/inbox/inbox.test.ts +171 -0
  14. package/src/inbox/index.ts +31 -0
  15. package/src/inbox/logic.ts +166 -0
  16. package/src/inbox/thread-inbox.svelte +363 -0
  17. package/src/inbox/types.ts +139 -0
  18. package/src/modules.test.ts +95 -0
  19. package/src/panel/README.md +5 -0
  20. package/src/panel/index.ts +2 -0
  21. package/src/{tasks → panel}/side-panel.svelte +1 -1
  22. package/src/record/README.md +3 -0
  23. package/src/{tasks/record → record}/field-section.svelte +1 -1
  24. package/src/{tasks/record → record}/info-list.svelte +9 -9
  25. package/src/{tasks/record → record}/next-up.svelte +4 -4
  26. package/src/{tasks/record → record}/project-list.svelte +5 -5
  27. package/src/{tasks/record → record}/record-demo.svelte +4 -4
  28. package/src/{tasks/record → record}/record-header.svelte +3 -3
  29. package/src/{tasks/record → record}/record-shell.svelte +4 -4
  30. package/src/{tasks/record → record}/record-stats.svelte +2 -2
  31. package/src/{tasks/record → record}/record-timeline.svelte +6 -6
  32. package/src/{tasks/record → record}/steps-rail.svelte +7 -7
  33. package/src/{tasks/record → record}/types.ts +2 -2
  34. package/src/{tasks/record → record}/where-we-are.svelte +4 -4
  35. package/src/status/README.md +18 -0
  36. package/src/status/index.ts +11 -0
  37. package/src/tasks/README.md +1 -1
  38. package/src/tasks/demo.svelte +3 -3
  39. package/src/tasks/detail/detail-demo.svelte +2 -2
  40. package/src/tasks/gantt/gantt.test.ts +1 -1
  41. package/src/tasks/inbox/inbox-card.svelte +2 -2
  42. package/src/tasks/inbox/inbox-statuses.ts +1 -1
  43. package/src/tasks/inbox/inbox.svelte +1 -1
  44. package/src/tasks/index.ts +9 -7
  45. package/src/tasks/presets/sales.ts +1 -1
  46. package/src/thread/README.md +31 -0
  47. package/src/thread/index.ts +12 -0
  48. package/src/thread/logic.ts +58 -0
  49. package/src/thread/thread.svelte +366 -0
  50. package/src/thread/thread.test.ts +62 -0
  51. package/src/thread/types.ts +93 -0
  52. /package/src/components/{list-page → section-header}/section-label.svelte +0 -0
  53. /package/src/components/{list-page → toolbar}/toolbar-search.svelte +0 -0
  54. /package/src/components/{list-page → toolbar}/toolbar-trigger.ts +0 -0
  55. /package/src/{tasks → panel}/side-panel-types.ts +0 -0
  56. /package/src/{tasks/record → record}/demo-data.ts +0 -0
  57. /package/src/{tasks/record → record}/fields.ts +0 -0
  58. /package/src/{tasks/record → record}/format.ts +0 -0
  59. /package/src/{tasks/record → record}/gutter.ts +0 -0
  60. /package/src/{tasks/record → record}/index.ts +0 -0
  61. /package/src/{tasks/record → record}/kinds.ts +0 -0
  62. /package/src/{tasks/record → record}/logic.ts +0 -0
  63. /package/src/{tasks/record → record}/record.test.ts +0 -0
  64. /package/src/{tasks → status}/define-statuses.ts +0 -0
  65. /package/src/{tasks → status}/status-badge.svelte +0 -0
  66. /package/src/{tasks → status}/status-dot.svelte +0 -0
  67. /package/src/{tasks → status}/status-icon.svelte +0 -0
  68. /package/src/{tasks → status}/status-types.ts +0 -0
@@ -2,11 +2,11 @@
2
2
  // =========================
3
3
  // IMPORTS
4
4
  // =========================
5
- import { cn } from "../../lib/utils.js";
6
- import { Progress } from "../../shadcn/progress/index.js";
7
- import { TONE_TEXT } from "../../lib/tone.js";
8
- import StatusBadge from "../status-badge.svelte";
9
- import { todayIso } from "../dates.js";
5
+ import { cn } from "../lib/utils.js";
6
+ import { Progress } from "../shadcn/progress/index.js";
7
+ import { TONE_TEXT } from "../lib/tone.js";
8
+ import StatusBadge from "../status/status-badge.svelte";
9
+ import { todayIso } from "../tasks/dates.js";
10
10
  import { formatMoney, formatRange } from "./format.js";
11
11
  import {
12
12
  resolveLabels,
@@ -3,10 +3,10 @@
3
3
  // IMPORTS
4
4
  // =========================
5
5
  import { DotsThree, NotePencil } from "phosphor-svelte";
6
- import { SegmentedFilter } from "../../components/segmented/index.js";
7
- import { Button } from "../../shadcn/button/index.js";
8
- import { Input } from "../../shadcn/input/index.js";
9
- import * as Tabs from "../../shadcn/tabs/index.js";
6
+ import { SegmentedFilter } from "../components/segmented/index.js";
7
+ import { Button } from "../shadcn/button/index.js";
8
+ import { Input } from "../shadcn/input/index.js";
9
+ import * as Tabs from "../shadcn/tabs/index.js";
10
10
  import RecordShell from "./record-shell.svelte";
11
11
  import RecordHeader from "./record-header.svelte";
12
12
  import WhereWeAre from "./where-we-are.svelte";
@@ -4,9 +4,9 @@
4
4
  // =========================
5
5
  import type { Snippet } from "svelte";
6
6
  import { CaretLeft } from "phosphor-svelte";
7
- import { cn } from "../../lib/utils.js";
8
- import type { Tone } from "../../lib/tone.js";
9
- import StatusBadge from "../status-badge.svelte";
7
+ import { cn } from "../lib/utils.js";
8
+ import type { Tone } from "../lib/tone.js";
9
+ import StatusBadge from "../status/status-badge.svelte";
10
10
  import { gutterClass } from "./gutter.js";
11
11
 
12
12
  // =========================
@@ -4,10 +4,10 @@
4
4
  // =========================
5
5
  import type { Snippet } from "svelte";
6
6
  import { CaretRight } from "phosphor-svelte";
7
- import { cn } from "../../lib/utils.js";
8
- import { AnimatedTabs, type TabDef } from "../../components/segmented/index.js";
9
- import * as Sheet from "../../shadcn/sheet/index.js";
10
- import { Progress } from "../../shadcn/progress/index.js";
7
+ import { cn } from "../lib/utils.js";
8
+ import { AnimatedTabs, type TabDef } from "../components/segmented/index.js";
9
+ import * as Sheet from "../shadcn/sheet/index.js";
10
+ import { Progress } from "../shadcn/progress/index.js";
11
11
  import { gutterClass } from "./gutter.js";
12
12
  import { resolveLabels, type RecordLabels } from "./types.js";
13
13
 
@@ -2,8 +2,8 @@
2
2
  // =========================
3
3
  // IMPORTS
4
4
  // =========================
5
- import { cn } from "../../lib/utils.js";
6
- import type { PanelStat } from "../side-panel-types.js";
5
+ import { cn } from "../lib/utils.js";
6
+ import type { PanelStat } from "../panel/side-panel-types.js";
7
7
  import { gutterClass } from "./gutter.js";
8
8
 
9
9
  // =========================
@@ -5,12 +5,12 @@
5
5
  import type { Snippet } from "svelte";
6
6
  import { Circle } from "phosphor-svelte";
7
7
  import { SvelteSet } from "svelte/reactivity";
8
- import { cn } from "../../lib/utils.js";
9
- import { Button } from "../../shadcn/button/index.js";
10
- import { Badge } from "../../shadcn/badge/index.js";
11
- import { Skeleton } from "../../shadcn/skeleton/index.js";
12
- import { SegmentedFilter } from "../../components/segmented/index.js";
13
- import { TONE_ICON } from "../../lib/tone.js";
8
+ import { cn } from "../lib/utils.js";
9
+ import { Button } from "../shadcn/button/index.js";
10
+ import { Badge } from "../shadcn/badge/index.js";
11
+ import { Skeleton } from "../shadcn/skeleton/index.js";
12
+ import { SegmentedFilter } from "../components/segmented/index.js";
13
+ import { TONE_ICON } from "../lib/tone.js";
14
14
  import { DEFAULT_RECORD_FILTERS, DEFAULT_RECORD_KINDS } from "./kinds.js";
15
15
  import { formatDayHeader, formatMoney, formatTime } from "./format.js";
16
16
  import {
@@ -12,13 +12,13 @@ import {
12
12
  EyeSlash,
13
13
  PencilSimple,
14
14
  } from "phosphor-svelte";
15
- import { cn } from "../../lib/utils.js";
16
- import { Button } from "../../shadcn/button/index.js";
17
- import { Input } from "../../shadcn/input/index.js";
18
- import { Skeleton } from "../../shadcn/skeleton/index.js";
19
- import * as Popover from "../../shadcn/popover/index.js";
20
- import * as ContextMenu from "../../shadcn/context-menu/index.js";
21
- import { todayIso } from "../dates.js";
15
+ import { cn } from "../lib/utils.js";
16
+ import { Button } from "../shadcn/button/index.js";
17
+ import { Input } from "../shadcn/input/index.js";
18
+ import { Skeleton } from "../shadcn/skeleton/index.js";
19
+ import * as Popover from "../shadcn/popover/index.js";
20
+ import * as ContextMenu from "../shadcn/context-menu/index.js";
21
+ import { todayIso } from "../tasks/dates.js";
22
22
  import { formatRange } from "./format.js";
23
23
  import {
24
24
  groupSteps,
@@ -1,7 +1,7 @@
1
1
  // The view types of a client or project record. Plain data in, nothing loaded:
2
2
  // the host maps its own rows onto these and passes them down.
3
- import type { PhosphorIcon } from "../../lib/icon-types.js";
4
- import type { Tone } from "../../lib/tone.js";
3
+ import type { PhosphorIcon } from "../lib/icon-types.js";
4
+ import type { Tone } from "../lib/tone.js";
5
5
 
6
6
  /** Where a stage sits in the pipeline. Structure, not an agency's own names. */
7
7
  export type StageCategory = "open" | "proposal" | "ongoing" | "completed" | "lost";
@@ -3,10 +3,10 @@
3
3
  // IMPORTS
4
4
  // =========================
5
5
  import { NavigationArrow } from "phosphor-svelte";
6
- import { cn } from "../../lib/utils.js";
7
- import { Button } from "../../shadcn/button/index.js";
8
- import StatusBadge from "../status-badge.svelte";
9
- import type { Tone } from "../../lib/tone.js";
6
+ import { cn } from "../lib/utils.js";
7
+ import { Button } from "../shadcn/button/index.js";
8
+ import StatusBadge from "../status/status-badge.svelte";
9
+ import type { Tone } from "../lib/tone.js";
10
10
  import { gutterClass } from "./gutter.js";
11
11
  import { formatAge } from "./format.js";
12
12
  import { resolveLabels, type RecordLabels } from "./types.js";
@@ -0,0 +1,18 @@
1
+ # @heyramzi/ui/status
2
+
3
+ Status badge, dot and icon from one map. `defineStatuses` builds the map, and the three parts read it, so a status looks the same everywhere. They used to live in `@heyramzi/ui/tasks`, which still re-exports every name.
4
+
5
+ ```svelte
6
+ <script lang="ts">
7
+ import { StatusBadge, defineStatuses } from "@heyramzi/ui/status";
8
+
9
+ const statuses = defineStatuses({
10
+ draft: { label: "Draft", tone: "neutral", variant: "open" },
11
+ sent: { label: "Sent", tone: "info", variant: "completed" },
12
+ });
13
+ </script>
14
+
15
+ <StatusBadge {statuses} status="sent" />
16
+ ```
17
+
18
+ Exports: `StatusBadge`, `StatusDot`, `StatusIcon`, `defineStatuses` and the types `StatusDef`, `StatusDotVariant`, `StatusId`, `StatusMap`, `Statuses`. The old subpaths `tasks/status`, `tasks/status-dot` and `tasks/statuses` resolve to the same files.
@@ -0,0 +1,11 @@
1
+ export { default as StatusBadge } from "./status-badge.svelte";
2
+ export { default as StatusDot } from "./status-dot.svelte";
3
+ export { default as StatusIcon } from "./status-icon.svelte";
4
+ export { defineStatuses } from "./define-statuses.js";
5
+ export type {
6
+ StatusDef,
7
+ StatusDotVariant,
8
+ StatusId,
9
+ StatusMap,
10
+ Statuses,
11
+ } from "./status-types.js";
@@ -204,7 +204,7 @@ icon, so `ChartBarHorizontal` stands in.
204
204
  `SidePanel` is a sheet for one record: a title, a description, status chips, doors
205
205
  out to other tools (`openIn`), a copy-a-prompt `handoff`, a strip of `stats`, then
206
206
  the body and an optional pinned `footer`. You pass the tools; the panel knows none.
207
- Import it from `@heyramzi/ui/tasks` or alone from `@heyramzi/ui/tasks/side-panel`.
207
+ Import it from `@heyramzi/ui/panel`, or from `@heyramzi/ui/tasks` for now.
208
208
 
209
209
  ```svelte
210
210
  <SidePanel
@@ -4,9 +4,9 @@
4
4
  import TaskGantt from "./task-gantt.svelte";
5
5
  import { SegmentedFilter } from "../components/segmented/index.js";
6
6
  import { ChartBarHorizontal, Kanban, ListBullets } from "phosphor-svelte";
7
- import StatusBadge from "./status-badge.svelte";
8
- import StatusDot from "./status-dot.svelte";
9
- import { defineStatuses } from "./define-statuses.js";
7
+ import StatusBadge from "../status/status-badge.svelte";
8
+ import StatusDot from "../status/status-dot.svelte";
9
+ import { defineStatuses } from "../status/define-statuses.js";
10
10
  import { addDays, dayNumber, todayIso } from "./dates.js";
11
11
  import { salesStatuses } from "./presets/sales.js";
12
12
  import type { KanbanMove } from "./kanban-types.js";
@@ -4,8 +4,8 @@
4
4
  import TaskProperty from "./task-property.svelte";
5
5
  import TaskChip from "./task-chip.svelte";
6
6
  import TimeTracker from "../time/time-tracker.svelte";
7
- import StatusDot from "../status-dot.svelte";
8
- import { defineStatuses } from "../define-statuses.js";
7
+ import StatusDot from "../../status/status-dot.svelte";
8
+ import { defineStatuses } from "../../status/define-statuses.js";
9
9
  import type { TaskComment } from "./detail-types.js";
10
10
  import type { TimeLogInput, TimeStopInfo } from "../time/time-types.js";
11
11
 
@@ -16,7 +16,7 @@ let app: Record<string, unknown> | undefined;
16
16
  function render(extra: Record<string, unknown> = {}) {
17
17
  target = document.createElement("div");
18
18
  document.body.append(target);
19
- app = mount(TaskGantt, {
19
+ app = mount(TaskGantt<Task>, {
20
20
  target,
21
21
  props: {
22
22
  items,
@@ -5,8 +5,8 @@
5
5
  import { Button } from "../../shadcn/button/index.js";
6
6
  import { Textarea } from "../../shadcn/textarea/index.js";
7
7
  import { cn } from "../../lib/utils.js";
8
- import StatusBadge from "../status-badge.svelte";
9
- import StatusDot from "../status-dot.svelte";
8
+ import StatusBadge from "../../status/status-badge.svelte";
9
+ import StatusDot from "../../status/status-dot.svelte";
10
10
  import { TONE_TEXT } from "../../lib/tone.js";
11
11
  import InboxDraftEditor from "./inbox-draft.svelte";
12
12
  import { canUndo, isOpen, startDraft } from "./inbox-logic.js";
@@ -1,4 +1,4 @@
1
- import { defineStatuses } from "../define-statuses.js";
1
+ import { defineStatuses } from "../../status/define-statuses.js";
2
2
  import { ClipboardText, EnvelopeSimple, Hash, Phone } from "phosphor-svelte";
3
3
  import type { PhosphorIcon } from "../../lib/icon-types.js";
4
4
  import type { InboxSourceType } from "./inbox-types.js";
@@ -7,7 +7,7 @@
7
7
  import { Tabs, TabsList, TabsTrigger } from "../../shadcn/tabs/index.js";
8
8
  import { ToggleGroup, ToggleGroupItem } from "../../shadcn/toggle-group/index.js";
9
9
  import { cn } from "../../lib/utils.js";
10
- import StatusDot from "../status-dot.svelte";
10
+ import StatusDot from "../../status/status-dot.svelte";
11
11
  import InboxCard from "./inbox-card.svelte";
12
12
  import { flatten, groupByProject, inLane, isTyping, laneOf } from "./inbox-logic.js";
13
13
  import { inboxKinds, inboxStates } from "./inbox-statuses.js";
@@ -13,19 +13,21 @@ export type {
13
13
  KanbanMove,
14
14
  KanbanSortKey,
15
15
  } from "./kanban-types.js";
16
- export { default as StatusBadge } from "./status-badge.svelte";
17
- export { default as StatusDot } from "./status-dot.svelte";
18
- export { default as StatusIcon } from "./status-icon.svelte";
19
- export { defineStatuses } from "./define-statuses.js";
16
+ export {
17
+ StatusBadge,
18
+ StatusDot,
19
+ StatusIcon,
20
+ defineStatuses,
21
+ } from "../status/index.js";
20
22
  export type {
21
23
  StatusDef,
22
24
  StatusDotVariant,
23
25
  StatusId,
24
26
  StatusMap,
25
27
  Statuses,
26
- } from "./status-types.js";
27
- export { default as SidePanel } from "./side-panel.svelte";
28
- export type { PanelStat, SidePanelWidth } from "./side-panel-types.js";
28
+ } from "../status/index.js";
29
+ export { SidePanel } from "../panel/index.js";
30
+ export type { PanelStat, SidePanelWidth } from "../panel/index.js";
29
31
  export { default as TaskList } from "./task-list.svelte";
30
32
  export { default as TaskGantt } from "./task-gantt.svelte";
31
33
  export { addDays, dayNumber, isoFromDay, todayIso } from "./dates.js";
@@ -1,4 +1,4 @@
1
- import { defineStatuses } from "../define-statuses.js";
1
+ import { defineStatuses } from "../../status/define-statuses.js";
2
2
 
3
3
  /**
4
4
  * An example status map for a sales pipeline. Names are neutral on purpose:
@@ -0,0 +1,31 @@
1
+ # @heyramzi/ui/thread
2
+
3
+ One comment thread for any subject: a task, a project, a doc or a diagram. The thread owns the layout and the draft state. It never fetches. You pass comments and callbacks, and it hands back what the person did.
4
+
5
+ ```svelte
6
+ <script lang="ts">
7
+ import { Thread, type ThreadComment, type ThreadPost } from "@heyramzi/ui/thread";
8
+
9
+ let comments = $state<ThreadComment[]>([]);
10
+
11
+ async function onPost({ body, parentId }: ThreadPost) {
12
+ const saved = await save(body, parentId);
13
+ if (!saved) return false; // keeps the draft
14
+ comments = [...comments, saved];
15
+ }
16
+ </script>
17
+
18
+ <Thread {comments} {onPost} {onEdit} {onDelete} {onResolve} canEdit={(c) => c.author.name === me} />
19
+ ```
20
+
21
+ - **Replies.** `parentId` points at the comment answered. A reply to a reply sits under the same root, a reply whose parent is gone becomes a root, and `buildThread(comments)` gives you the same grouping without the component.
22
+ - **Authors.** `author.kind` is `user` or `agent`. An agent shows an "AI" chip. The `avatar` snippet replaces the face for both kinds, so an app passes its cast portrait there.
23
+ - **Rich bodies.** A comment with `content` (Tiptap JSON) draws through `@heyramzi/ui/editor`'s renderer. `body` stays the plain-text fallback. A rich comment has no edit button, because the thread edits text only.
24
+ - **Resolve.** Pass `onResolve` and each root gets a check. Resolved roots fold away behind a "Show N resolved" button.
25
+ - **Anchor.** `anchor` carries `{ nodeId, dx, dy, x, y }` for a canvas pin. A pinned comment shows a chip, and `onAnchor` fires on a click so the canvas can move to the node. `anchorChip` replaces the chip.
26
+ - **Attachments.** The `attachments` snippet draws under a comment's body.
27
+ - **Labels.** Every string comes from `labels`, a partial of `ThreadLabels`. `showResolved` holds a `{count}` slot. `when(comment)` formats the time.
28
+
29
+ ```ts
30
+ const fr = { title: "Activité", send: "Commenter", showResolved: "Afficher {count} résolu" };
31
+ ```
@@ -0,0 +1,12 @@
1
+ export { default as Thread } from "./thread.svelte";
2
+ export { buildThread, fillCount, resolveThreadLabels, resolvedCount } from "./logic.js";
3
+ export { DEFAULT_THREAD_LABELS } from "./types.js";
4
+ export type {
5
+ ThreadAnchor,
6
+ ThreadAuthor,
7
+ ThreadAuthorKind,
8
+ ThreadComment,
9
+ ThreadLabels,
10
+ ThreadNode,
11
+ ThreadPost,
12
+ } from "./types.js";
@@ -0,0 +1,58 @@
1
+ import type { ThreadComment, ThreadLabels, ThreadNode } from "./types.js";
2
+ import { DEFAULT_THREAD_LABELS } from "./types.js";
3
+
4
+ function time(comment: ThreadComment): number {
5
+ const ms = Date.parse(comment.createdAt);
6
+ return Number.isNaN(ms) ? Number.POSITIVE_INFINITY : ms;
7
+ }
8
+
9
+ /** Oldest first, a malformed timestamp last, ties in the order they came. */
10
+ function byTime(comments: ThreadComment[]): ThreadComment[] {
11
+ return comments
12
+ .map((comment, index) => ({ comment, index }))
13
+ .toSorted((a, b) => time(a.comment) - time(b.comment) || a.index - b.index)
14
+ .map((row) => row.comment);
15
+ }
16
+
17
+ /**
18
+ * Roots in time order, each with its replies. A reply to a reply joins the root's list, a reply whose
19
+ * parent is gone becomes a root, and a loop of parents can't hang the walk.
20
+ */
21
+ export function buildThread(comments: ThreadComment[]): ThreadNode[] {
22
+ const byId = new Map(comments.map((comment) => [comment.id, comment]));
23
+
24
+ function rootOf(comment: ThreadComment): ThreadComment {
25
+ const seen = new Set<string>([comment.id]);
26
+ let current = comment;
27
+ for (;;) {
28
+ const parent = current.parentId ? byId.get(current.parentId) : undefined;
29
+ if (!parent) return current;
30
+ if (seen.has(parent.id)) return comment;
31
+ seen.add(parent.id);
32
+ current = parent;
33
+ }
34
+ }
35
+
36
+ const replies = new Map<string, ThreadComment[]>();
37
+ const roots: ThreadComment[] = [];
38
+ for (const comment of comments) {
39
+ const root = rootOf(comment);
40
+ if (root.id === comment.id) roots.push(comment);
41
+ else replies.set(root.id, [...(replies.get(root.id) ?? []), comment]);
42
+ }
43
+ return byTime(roots).map((comment) => ({ comment, replies: byTime(replies.get(comment.id) ?? []) }));
44
+ }
45
+
46
+ /** How many conversations are resolved. */
47
+ export function resolvedCount(nodes: ThreadNode[]): number {
48
+ return nodes.filter((node) => node.comment.resolved).length;
49
+ }
50
+
51
+ export function resolveThreadLabels(overrides?: Partial<ThreadLabels>): ThreadLabels {
52
+ return { ...DEFAULT_THREAD_LABELS, ...overrides };
53
+ }
54
+
55
+ /** Fills `{count}` in a label, so a French host writes its own sentence. */
56
+ export function fillCount(label: string, count: number): string {
57
+ return label.replaceAll("{count}", String(count));
58
+ }