@tomehq/theme 0.6.4 → 0.7.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.
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
2
+ import React from "react";
3
+ import { render, screen, fireEvent } from "@testing-library/react";
4
+ import Shell from "../Shell.js";
5
+
6
+ // jsdom doesn't provide matchMedia
7
+ beforeAll(() => {
8
+ Object.defineProperty(window, "matchMedia", {
9
+ writable: true,
10
+ value: vi.fn().mockImplementation((query: string) => ({
11
+ matches: query.includes("dark"),
12
+ media: query,
13
+ onchange: null,
14
+ addListener: vi.fn(),
15
+ removeListener: vi.fn(),
16
+ addEventListener: vi.fn(),
17
+ removeEventListener: vi.fn(),
18
+ dispatchEvent: vi.fn(),
19
+ })),
20
+ });
21
+ });
22
+
23
+ // ── Minimal Shell props for feedback widget testing ──────
24
+
25
+ const baseConfig = {
26
+ name: "Test Docs",
27
+ theme: { preset: "amber", mode: "auto" },
28
+ toc: { enabled: false },
29
+ };
30
+
31
+ const navigation = [{
32
+ section: "Docs",
33
+ pages: [{ id: "test", title: "Test Page", urlPath: "/test" }],
34
+ }];
35
+
36
+ const allPages = [{ id: "test", title: "Test Page", description: "A test page" }];
37
+
38
+ function renderShell(configOverrides = {}) {
39
+ return render(
40
+ <Shell
41
+ config={{ ...baseConfig, ...configOverrides }}
42
+ navigation={navigation}
43
+ currentPageId="test"
44
+ pageHtml="<h1>Test Page</h1><p>Content here.</p>"
45
+ pageTitle="Test Page"
46
+ headings={[]}
47
+ allPages={allPages}
48
+ onNavigate={() => {}}
49
+ />
50
+ );
51
+ }
52
+
53
+ // ── Setup ────────────────────────────────────────────────
54
+
55
+ beforeEach(() => {
56
+ localStorage.clear();
57
+ (window as any).__tome = {
58
+ trackFeedback: vi.fn(),
59
+ trackSearch: vi.fn(),
60
+ };
61
+ });
62
+
63
+ // ── Tests ────────────────────────────────────────────────
64
+
65
+ describe("FeedbackWidget", () => {
66
+ it("renders thumbs up and down buttons by default", () => {
67
+ renderShell();
68
+ expect(screen.getByTestId("feedback-up")).toBeInTheDocument();
69
+ expect(screen.getByTestId("feedback-down")).toBeInTheDocument();
70
+ expect(screen.getByText("Was this helpful?")).toBeInTheDocument();
71
+ });
72
+
73
+ it("hides feedback widget when feedback.enabled is false", () => {
74
+ renderShell({ feedback: { enabled: false } });
75
+ expect(screen.queryByTestId("feedback-up")).not.toBeInTheDocument();
76
+ expect(screen.queryByText("Was this helpful?")).not.toBeInTheDocument();
77
+ });
78
+
79
+ it("shows thanks message after thumbs up without textInput", () => {
80
+ renderShell();
81
+ fireEvent.click(screen.getByTestId("feedback-up"));
82
+ expect(screen.getByText("Thanks for your feedback!")).toBeInTheDocument();
83
+ expect(screen.queryByTestId("feedback-up")).not.toBeInTheDocument();
84
+ });
85
+
86
+ it("fires trackFeedback on thumbs up without textInput", () => {
87
+ renderShell();
88
+ fireEvent.click(screen.getByTestId("feedback-up"));
89
+ expect((window as any).__tome.trackFeedback).toHaveBeenCalledWith("test", "up");
90
+ });
91
+
92
+ it("fires trackFeedback on thumbs down without textInput", () => {
93
+ renderShell();
94
+ fireEvent.click(screen.getByTestId("feedback-down"));
95
+ expect((window as any).__tome.trackFeedback).toHaveBeenCalledWith("test", "down");
96
+ });
97
+
98
+ it("stores feedback in localStorage", () => {
99
+ renderShell();
100
+ fireEvent.click(screen.getByTestId("feedback-up"));
101
+ expect(localStorage.getItem("tome-feedback-test")).toBe("up");
102
+ });
103
+
104
+ it("shows text input after thumbs up when textInput is enabled", () => {
105
+ renderShell({ feedback: { enabled: true, textInput: true } });
106
+ fireEvent.click(screen.getByTestId("feedback-up"));
107
+ expect(screen.getByTestId("feedback-text-input")).toBeInTheDocument();
108
+ expect(screen.getByTestId("feedback-submit")).toBeInTheDocument();
109
+ expect(screen.getByText("Any additional feedback? (optional)")).toBeInTheDocument();
110
+ });
111
+
112
+ it("submits text feedback and shows thanks", () => {
113
+ renderShell({ feedback: { enabled: true, textInput: true } });
114
+ fireEvent.click(screen.getByTestId("feedback-up"));
115
+
116
+ const input = screen.getByTestId("feedback-text-input");
117
+ fireEvent.change(input, { target: { value: "Great docs!" } });
118
+ fireEvent.click(screen.getByTestId("feedback-submit"));
119
+
120
+ expect((window as any).__tome.trackFeedback).toHaveBeenCalledWith("test", "up", "Great docs!");
121
+ expect(screen.getByText("Thanks for your feedback!")).toBeInTheDocument();
122
+ });
123
+
124
+ it("skip button submits without comment", () => {
125
+ renderShell({ feedback: { enabled: true, textInput: true } });
126
+ fireEvent.click(screen.getByTestId("feedback-down"));
127
+ fireEvent.click(screen.getByText("Skip"));
128
+
129
+ expect((window as any).__tome.trackFeedback).toHaveBeenCalledWith("test", "down");
130
+ expect(screen.getByText("Thanks for your feedback!")).toBeInTheDocument();
131
+ });
132
+ });
package/src/ai-api.ts ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Shared AI API utilities for Tome's AI features (chat + search).
3
+ * Supports OpenAI and Anthropic providers with BYOK (Bring Your Own Key).
4
+ */
5
+
6
+ export type AiProvider = "openai" | "anthropic" | "custom";
7
+
8
+ export interface AiMessage {
9
+ role: "user" | "assistant";
10
+ content: string;
11
+ }
12
+
13
+ /**
14
+ * Build a system prompt with optional documentation context.
15
+ */
16
+ export function buildSystemPrompt(context?: string, instruction?: string): string {
17
+ const base = instruction || "You are a helpful documentation assistant. Answer questions accurately based on the documentation provided below. If the answer isn't in the documentation, say so clearly. Keep answers concise and reference specific sections when possible.";
18
+ if (!context) return base;
19
+ // Truncate context to stay within token limits (~100K chars ≈ 25K tokens)
20
+ const trimmed = context.length > 100000 ? context.slice(0, 100000) + "\n\n[Documentation truncated...]" : context;
21
+ return `${base}\n\nDocumentation:\n${trimmed}`;
22
+ }
23
+
24
+ /**
25
+ * Call OpenAI's chat completions API.
26
+ */
27
+ export async function callOpenAI(
28
+ messages: AiMessage[],
29
+ apiKey: string,
30
+ model: string,
31
+ systemPrompt: string,
32
+ ): Promise<string> {
33
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
34
+ method: "POST",
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ "Authorization": `Bearer ${apiKey}`,
38
+ },
39
+ body: JSON.stringify({
40
+ model,
41
+ messages: [
42
+ { role: "system", content: systemPrompt },
43
+ ...messages.map((m) => ({ role: m.role, content: m.content })),
44
+ ],
45
+ }),
46
+ });
47
+ if (!res.ok) {
48
+ const err = await res.text();
49
+ throw new Error(`OpenAI API error (${res.status}): ${err}`);
50
+ }
51
+ const data = await res.json();
52
+ return data.choices?.[0]?.message?.content || "No response.";
53
+ }
54
+
55
+ /**
56
+ * Call Anthropic's messages API.
57
+ */
58
+ export async function callAnthropic(
59
+ messages: AiMessage[],
60
+ apiKey: string,
61
+ model: string,
62
+ systemPrompt: string,
63
+ ): Promise<string> {
64
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
65
+ method: "POST",
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ "x-api-key": apiKey,
69
+ "anthropic-version": "2023-06-01",
70
+ "anthropic-dangerous-direct-browser-access": "true",
71
+ },
72
+ body: JSON.stringify({
73
+ model,
74
+ max_tokens: 1024,
75
+ system: systemPrompt,
76
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
77
+ }),
78
+ });
79
+ if (!res.ok) {
80
+ const err = await res.text();
81
+ throw new Error(`Anthropic API error (${res.status}): ${err}`);
82
+ }
83
+ const data = await res.json();
84
+ return data.content?.[0]?.text || "No response.";
85
+ }
86
+
87
+ /**
88
+ * Get the default model for a provider.
89
+ */
90
+ export function getDefaultModel(provider: AiProvider): string {
91
+ if (provider === "openai") return "gpt-4o-mini";
92
+ return "claude-sonnet-4-20250514";
93
+ }
94
+
95
+ /**
96
+ * Call the appropriate AI provider with a query and context.
97
+ * Returns the AI's response text.
98
+ */
99
+ export async function callAiProvider(
100
+ provider: AiProvider,
101
+ messages: AiMessage[],
102
+ apiKey: string,
103
+ model: string,
104
+ systemPrompt: string,
105
+ ): Promise<string> {
106
+ if (provider === "openai") {
107
+ return callOpenAI(messages, apiKey, model, systemPrompt);
108
+ }
109
+ return callAnthropic(messages, apiKey, model, systemPrompt);
110
+ }
@@ -52,6 +52,7 @@ export interface ApiReferencePage {
52
52
  headings: Array<{ depth: number; text: string; id: string }>;
53
53
  changelogEntries?: undefined;
54
54
  apiManifest: any;
55
+ asyncApiManifest?: any;
55
56
  }
56
57
 
57
58
  export type LoadedPage = HtmlPage | MdxPage | ApiReferencePage;
@@ -124,9 +125,13 @@ export async function loadPage(
124
125
  // Regular .md page — mod.default is { html, frontmatter, headings }
125
126
  if (!mod.default) throw new PageNotFoundError(id);
126
127
 
127
- // API reference page (synthetic route from OpenAPI spec)
128
- if (mod.isApiReference && mod.apiManifest) {
129
- return { isMdx: false, isApiReference: true, ...mod.default, apiManifest: mod.apiManifest };
128
+ // API reference page (synthetic route from OpenAPI/AsyncAPI spec)
129
+ if (mod.isApiReference && (mod.apiManifest || mod.asyncApiManifest)) {
130
+ return {
131
+ isMdx: false, isApiReference: true, ...mod.default,
132
+ apiManifest: mod.apiManifest,
133
+ asyncApiManifest: mod.asyncApiManifest,
134
+ };
130
135
  }
131
136
 
132
137
  // Changelog page type
@@ -151,6 +151,12 @@ vi.mock("@tomehq/components", () => ({
151
151
  LinkCard: () => <div />,
152
152
  CardGrid: () => <div />,
153
153
  ApiReference: MockApiReference,
154
+ AsyncApiReference: (p: any) => <div data-testid="async-api-ref" />,
155
+ ProtocolBadge: () => <div />,
156
+ DirectionBadge: () => <div />,
157
+ ChannelCard: () => <div />,
158
+ MessageBlock: () => <div />,
159
+ AsyncParameterTable: () => <div />,
154
160
  }));
155
161
 
156
162
  // ── Global stubs ─────────────────────────────────────────
package/src/entry.tsx CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  LinkCard,
39
39
  CardGrid,
40
40
  ApiReference,
41
+ AsyncApiReference,
41
42
  } from "@tomehq/components";
42
43
 
43
44
  const MDX_COMPONENTS: Record<string, React.ComponentType<any>> = {
@@ -575,10 +576,12 @@ function App() {
575
576
  lastUpdated={currentRoute?.lastUpdated}
576
577
  changelogEntries={!pageData?.isMdx ? pageData?.changelogEntries : undefined}
577
578
  apiManifest={(!pageData?.isMdx && pageData?.isApiReference) ? pageData.apiManifest : undefined}
579
+ asyncApiManifest={(!pageData?.isMdx && pageData?.isApiReference) ? (pageData as any).asyncApiManifest : undefined}
578
580
  apiBaseUrl={config.api?.baseUrl}
579
581
  apiPlayground={config.api?.playground}
580
582
  apiAuth={config.api?.auth}
581
583
  ApiReferenceComponent={ApiReference}
584
+ AsyncApiReferenceComponent={AsyncApiReference}
582
585
  onNavigate={navigateTo}
583
586
  allPages={allPages}
584
587
  docContext={docContext}
package/src/presets.ts CHANGED
@@ -49,13 +49,13 @@ export const THEME_PRESETS = {
49
49
  cipher: {
50
50
  dark: {
51
51
  bg:"#050508",sf:"#0c0c12",sfH:"#12121a",bd:"#1a1a25",
52
- tx:"#d4ff00",tx2:"#8a90a0",txM:"#6a7080",
52
+ tx:"#d4ff00",tx2:"#8a90a0",txM:"#7e8494",
53
53
  ac:"#6666ff",acD:"rgba(102,102,255,0.10)",acT:"#8080ff",
54
54
  cdBg:"#08080e",cdTx:"#b0c870",sbBg:"#08080d",hdBg:"rgba(5,5,8,0.88)",
55
55
  },
56
56
  light: {
57
57
  bg:"#f0f2f5",sf:"#ffffff",sfH:"#e8eaef",bd:"#d0d4db",
58
- tx:"#0f1219",tx2:"#4a5060",txM:"#6a7080",
58
+ tx:"#0f1219",tx2:"#4a5060",txM:"#5c6272",
59
59
  ac:"#2020cc",acD:"rgba(32,32,204,0.08)",acT:"#1a1aa8",
60
60
  cdBg:"#e6e9ef",cdTx:"#2a3520",sbBg:"#ebedf2",hdBg:"rgba(240,242,245,0.90)",
61
61
  },
@@ -64,7 +64,7 @@ export const THEME_PRESETS = {
64
64
  mint: {
65
65
  dark: {
66
66
  bg:"#0d1117",sf:"#161b22",sfH:"#1c2129",bd:"#21262d",
67
- tx:"#e6edf3",tx2:"#8b949e",txM:"#6e7681",
67
+ tx:"#e6edf3",tx2:"#8b949e",txM:"#7d858f",
68
68
  ac:"#0ea371",acD:"rgba(14,163,113,0.10)",acT:"#2dd4a0",
69
69
  cdBg:"#0a0e14",cdTx:"#adbac7",sbBg:"#0d1117",hdBg:"rgba(13,17,23,0.88)",
70
70
  },
@@ -76,6 +76,96 @@ export const THEME_PRESETS = {
76
76
  },
77
77
  fonts: { heading: "Inter", body: "Inter", code: "Fira Code" },
78
78
  },
79
+ ocean: {
80
+ dark: {
81
+ bg:"#0a1628",sf:"#0f1d33",sfH:"#142540",bd:"#1a2e50",
82
+ tx:"#e0e8f0",tx2:"#8ca0b8",txM:"#7b92aa",
83
+ ac:"#0ea5e9",acD:"rgba(14,165,233,0.10)",acT:"#38bdf8",
84
+ cdBg:"#081320",cdTx:"#94b4d0",sbBg:"#0a1628",hdBg:"rgba(10,22,40,0.90)",
85
+ },
86
+ light: {
87
+ bg:"#f0f7ff",sf:"#ffffff",sfH:"#e8f0fa",bd:"#ccdcef",
88
+ tx:"#0c1929",tx2:"#3d5a78",txM:"#4a6a88",
89
+ ac:"#0369a1",acD:"rgba(3,105,161,0.07)",acT:"#025e8f",
90
+ cdBg:"#e4edf7",cdTx:"#1a3050",sbBg:"#eef4fc",hdBg:"rgba(240,247,255,0.92)",
91
+ },
92
+ fonts: { heading: "Outfit", body: "Inter", code: "JetBrains Mono" },
93
+ },
94
+ rose: {
95
+ dark: {
96
+ bg:"#0f0a10",sf:"#171118",sfH:"#1e1620",bd:"#2a1f2c",
97
+ tx:"#f0e4f0",tx2:"#b09ab0",txM:"#8a7890",
98
+ ac:"#f43f5e",acD:"rgba(244,63,94,0.10)",acT:"#fb7185",
99
+ cdBg:"#0d080e",cdTx:"#c8aec8",sbBg:"#0f0a10",hdBg:"rgba(15,10,16,0.88)",
100
+ },
101
+ light: {
102
+ bg:"#fef7f7",sf:"#ffffff",sfH:"#fceef0",bd:"#f0d4d8",
103
+ tx:"#1a0f12",tx2:"#6b4048",txM:"#8a5a64",
104
+ ac:"#c81e3e",acD:"rgba(200,30,62,0.06)",acT:"#a81835",
105
+ cdBg:"#f8eaec",cdTx:"#3a1a22",sbBg:"#fdf2f4",hdBg:"rgba(254,247,247,0.92)",
106
+ },
107
+ fonts: { heading: "Playfair Display", body: "Source Sans 3", code: "Fira Code" },
108
+ },
109
+ forest: {
110
+ dark: {
111
+ bg:"#091209",sf:"#0f1a0f",sfH:"#152215",bd:"#1e2e1e",
112
+ tx:"#e0f0e0",tx2:"#8aaa8a",txM:"#6a8a6a",
113
+ ac:"#22c55e",acD:"rgba(34,197,94,0.10)",acT:"#4ade80",
114
+ cdBg:"#070e07",cdTx:"#a0c4a0",sbBg:"#091209",hdBg:"rgba(9,18,9,0.90)",
115
+ },
116
+ light: {
117
+ bg:"#f4faf4",sf:"#ffffff",sfH:"#e8f4e8",bd:"#c8e0c8",
118
+ tx:"#0a1a0a",tx2:"#3a5a3a",txM:"#5a7a5a",
119
+ ac:"#15803d",acD:"rgba(21,128,61,0.07)",acT:"#116d34",
120
+ cdBg:"#e4f2e4",cdTx:"#1a3a1a",sbBg:"#eef6ee",hdBg:"rgba(244,250,244,0.92)",
121
+ },
122
+ fonts: { heading: "Merriweather", body: "Nunito Sans", code: "Source Code Pro" },
123
+ },
124
+ slate: {
125
+ dark: {
126
+ bg:"#0f1115",sf:"#16181e",sfH:"#1c1f26",bd:"#24272e",
127
+ tx:"#e2e4e8",tx2:"#9498a0",txM:"#808690",
128
+ ac:"#94a3b8",acD:"rgba(148,163,184,0.10)",acT:"#b0bec8",
129
+ cdBg:"#0c0e12",cdTx:"#a8acb4",sbBg:"#0f1115",hdBg:"rgba(15,17,21,0.88)",
130
+ },
131
+ light: {
132
+ bg:"#f8fafc",sf:"#ffffff",sfH:"#f1f5f9",bd:"#d8dfe7",
133
+ tx:"#0f172a",tx2:"#475569",txM:"#64748b",
134
+ ac:"#475569",acD:"rgba(71,85,105,0.07)",acT:"#3a4a5c",
135
+ cdBg:"#f0f4f8",cdTx:"#1e293b",sbBg:"#f5f7fa",hdBg:"rgba(248,250,252,0.92)",
136
+ },
137
+ fonts: { heading: "Inter", body: "Inter", code: "JetBrains Mono" },
138
+ },
139
+ sunset: {
140
+ dark: {
141
+ bg:"#120c06",sf:"#1a1208",sfH:"#22180c",bd:"#2e2010",
142
+ tx:"#f0e4d4",tx2:"#b0986a",txM:"#907850",
143
+ ac:"#f97316",acD:"rgba(249,115,22,0.10)",acT:"#fb923c",
144
+ cdBg:"#0e0a05",cdTx:"#c8aa78",sbBg:"#120c06",hdBg:"rgba(18,12,6,0.90)",
145
+ },
146
+ light: {
147
+ bg:"#fffbf5",sf:"#ffffff",sfH:"#fef3e6",bd:"#f0d8b8",
148
+ tx:"#1a1008",tx2:"#6a5030",txM:"#8a6840",
149
+ ac:"#c2410c",acD:"rgba(194,65,12,0.06)",acT:"#a63a0a",
150
+ cdBg:"#faf0e2",cdTx:"#3a2810",sbBg:"#fdf6ec",hdBg:"rgba(255,251,245,0.92)",
151
+ },
152
+ fonts: { heading: "Sora", body: "DM Sans", code: "Fira Code" },
153
+ },
154
+ carbon: {
155
+ dark: {
156
+ bg:"#080808",sf:"#101010",sfH:"#171717",bd:"#1f1f1f",
157
+ tx:"#d4d4d4",tx2:"#888888",txM:"#787878",
158
+ ac:"#e4e4e4",acD:"rgba(228,228,228,0.08)",acT:"#f0f0f0",
159
+ cdBg:"#0a0a0a",cdTx:"#a0a0a0",sbBg:"#080808",hdBg:"rgba(8,8,8,0.90)",
160
+ },
161
+ light: {
162
+ bg:"#f5f5f5",sf:"#ffffff",sfH:"#ebebeb",bd:"#d4d4d4",
163
+ tx:"#171717",tx2:"#525252",txM:"#6b6b6b",
164
+ ac:"#262626",acD:"rgba(38,38,38,0.06)",acT:"#1a1a1a",
165
+ cdBg:"#eaeaea",cdTx:"#1a1a1a",sbBg:"#f0f0f0",hdBg:"rgba(245,245,245,0.92)",
166
+ },
167
+ fonts: { heading: "Geist", body: "Geist", code: "Geist Mono" },
168
+ },
79
169
  } as const satisfies Record<string, ThemePreset>;
80
170
 
81
171
  export type PresetName = keyof typeof THEME_PRESETS;
@@ -61,9 +61,8 @@ describe("pathnameToPageId", () => {
61
61
 
62
62
  it("resolves versioned index", () => {
63
63
  // /docs/v1/ → strip basePath → /v1/ → strip leading / → v1/ → strip trailing / → v1
64
- // But our route ID is "v1/index", not "v1", so this needs to resolve properly
65
- // After all stripping: "v1" not in routes (v1/index is). Let's test this edge case.
66
- expect(pathnameToPageId("/docs/v1", basePath, routes)).toBeNull();
64
+ // Route v1/index has urlPath "/v1/" matches via URL-based resolution
65
+ expect(pathnameToPageId("/docs/v1", basePath, routes)).toBe("v1/index");
67
66
  });
68
67
 
69
68
  describe("with empty basePath", () => {
package/src/routing.ts CHANGED
@@ -18,8 +18,10 @@ export function pathnameToPageId(
18
18
  routes: MinimalRoute[],
19
19
  ): string | null {
20
20
  let relative = pathname;
21
- if (basePath && relative.startsWith(basePath)) {
22
- relative = relative.slice(basePath.length);
21
+ // Handle basePath stripping — treat "/" basePath as empty to avoid stripping all leading slashes
22
+ const normalizedBase = basePath === "/" ? "" : basePath.replace(/\/+$/, "");
23
+ if (normalizedBase && relative.startsWith(normalizedBase)) {
24
+ relative = relative.slice(normalizedBase.length);
23
25
  }
24
26
  const id =
25
27
  relative
@@ -27,8 +29,14 @@ export function pathnameToPageId(
27
29
  .replace(/\/index\.html$/, "")
28
30
  .replace(/\.html$/, "")
29
31
  .replace(/\/$/, "") || "index";
30
- const route = routes.find((r) => r.id === id);
31
- return route ? id : null;
32
+ // Match by ID first (most common: urlPath matches ID)
33
+ const routeById = routes.find((r) => r.id === id);
34
+ if (routeById) return id;
35
+ // Match by urlPath (for synthetic routes where ID differs from URL, e.g. api-reference at /events-api)
36
+ const urlPath = "/" + id;
37
+ const urlPathWithSlash = urlPath + "/";
38
+ const routeByUrl = routes.find((r) => r.urlPath === urlPath || r.urlPath === urlPathWithSlash);
39
+ return routeByUrl ? routeByUrl.id : null;
32
40
  }
33
41
 
34
42
  /**