@mdguggenbichler/slugbase-core 0.0.38 → 0.0.40

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.
@@ -1 +1 @@
1
- {"version":3,"file":"app-factory.d.ts","sourceRoot":"","sources":["../src/app-factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,OAAO,MAAM,SAAS,CAAC;AAc9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAGhE,MAAM,WAAW,gBAAgB;IAC/B,6HAA6H;IAC7H,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,YAAY,KAAK,IAAI,CAAC;IACrG,qEAAqE;IACrE,YAAY,EAAE,YAAY,CAAC,OAAO,oBAAoB,CAAC,CAAC;CACzD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CA4FpE"}
1
+ {"version":3,"file":"app-factory.d.ts","sourceRoot":"","sources":["../src/app-factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,OAAO,MAAM,SAAS,CAAC;AAc9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAGhE,MAAM,WAAW,gBAAgB;IAC/B,6HAA6H;IAC7H,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,YAAY,KAAK,IAAI,CAAC;IACrG,qEAAqE;IACrE,YAAY,EAAE,YAAY,CAAC,OAAO,oBAAoB,CAAC,CAAC;CACzD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CA8FpE"}
@@ -26,8 +26,10 @@ export function createApp(options) {
26
26
  frontendUrl,
27
27
  'http://localhost:3000',
28
28
  'http://localhost:3001',
29
+ 'http://localhost:3002', // e2e frontend (run-e2e.js uses port 3002)
29
30
  'http://127.0.0.1:3000',
30
31
  'http://127.0.0.1:3001',
32
+ 'http://127.0.0.1:3002',
31
33
  ];
32
34
  const extraOrigins = (process.env.CORS_EXTRA_ORIGINS || '')
33
35
  .split(',')
@@ -1 +1 @@
1
- {"version":3,"file":"app-factory.js","sourceRoot":"","sources":["../src/app-factory.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app-factory.js","sourceRoot":"","sources":["../src/app-factory.ts"],"names":[],"mappings":"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"}
@@ -50,6 +50,18 @@ function AdminRoute({ children }: { children: React.ReactNode }) {
50
50
  return <>{children}</>;
51
51
  }
52
52
 
53
+ function ExtraAdminRoutes() {
54
+ const { extraAdminRoutes } = useAppConfig();
55
+ if (!extraAdminRoutes?.length) return null;
56
+ return (
57
+ <>
58
+ {extraAdminRoutes.map(({ path, element }) => (
59
+ <Route key={path} path={path} element={element} />
60
+ ))}
61
+ </>
62
+ );
63
+ }
64
+
53
65
  function SharedRedirect() {
54
66
  const { pathPrefixForLinks } = useAppConfig();
55
67
  const to = `${pathPrefixForLinks || ''}/bookmarks?scope=shared_with_me`.replace(/\/+/g, '/') || '/bookmarks?scope=shared_with_me';
@@ -137,6 +149,7 @@ function AppRoutes() {
137
149
  </>
138
150
  )}
139
151
  <Route path="ai" element={<AdminAIPage />} />
152
+ <ExtraAdminRoutes />
140
153
  </Route>
141
154
  </Route>
142
155
  </Routes>
@@ -243,9 +256,13 @@ export interface AppProps {
243
256
  hideAdminOidcAndSmtp?: boolean;
244
257
  /** When true, Admin AI page shows only the enable/disable toggle (e.g. cloud uses env for provider/model/key). */
245
258
  adminAiOnlyToggle?: boolean;
259
+ /** Optional extra admin routes (e.g. cloud billing). Generic extension; path is segment under /admin. */
260
+ extraAdminRoutes?: { path: string; element: ReactNode }[];
261
+ /** Optional extra admin nav items (e.g. cloud billing). Passed to sidebar when extraAdminRoutes are used. */
262
+ extraAdminNavItems?: { path: string; label: string }[];
246
263
  }
247
264
 
248
- function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow, hideAdminOidcAndSmtp, adminAiOnlyToggle }: AppProps = {}) {
265
+ function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow, hideAdminOidcAndSmtp, adminAiOnlyToggle, extraAdminRoutes, extraAdminNavItems }: AppProps = {}) {
249
266
  const appRootPath = routerBasename !== undefined ? '/' : (basePath === '/' || !basePath ? '/' : basePath);
250
267
  const pathPrefixForLinks = routerBasename !== undefined ? '' : (basePath ?? '');
251
268
  const content = (
@@ -261,7 +278,7 @@ function App({ basePath, apiBaseUrl, routerBasename, skipSetupFlow, hideAdminOid
261
278
  );
262
279
  return (
263
280
  <AppErrorBoundary>
264
- <AppConfigProvider appBasePath={basePath} apiBaseUrl={apiBaseUrl} appRootPath={appRootPath} skipSetupFlow={skipSetupFlow} pathPrefixForLinks={pathPrefixForLinks} hideAdminOidcAndSmtp={hideAdminOidcAndSmtp} adminAiOnlyToggle={adminAiOnlyToggle}>
281
+ <AppConfigProvider appBasePath={basePath} apiBaseUrl={apiBaseUrl} appRootPath={appRootPath} skipSetupFlow={skipSetupFlow} pathPrefixForLinks={pathPrefixForLinks} hideAdminOidcAndSmtp={hideAdminOidcAndSmtp} adminAiOnlyToggle={adminAiOnlyToggle} extraAdminNavItems={extraAdminNavItems} extraAdminRoutes={extraAdminRoutes}>
265
282
  {routerBasename !== undefined ? (
266
283
  content
267
284
  ) : (
@@ -15,6 +15,7 @@ import {
15
15
  UserCog,
16
16
  Key,
17
17
  Sparkles,
18
+ CreditCard,
18
19
  } from 'lucide-react';
19
20
  import {
20
21
  Sidebar,
@@ -43,7 +44,7 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
43
44
  const { t } = useTranslation();
44
45
  const location = useLocation();
45
46
  const pathname = location.pathname;
46
- const { appBasePath, pathPrefixForLinks, hideAdminOidcAndSmtp } = useAppConfig();
47
+ const { appBasePath, pathPrefixForLinks, hideAdminOidcAndSmtp, extraAdminNavItems } = useAppConfig();
47
48
  const { setOpenMobile, toggleSidebar, isMobile, state } = useSidebar();
48
49
  const prefix = pathPrefixForLinks || '';
49
50
  // For active matching use pathPrefixForLinks so it matches useLocation().pathname (e.g. when Router has basename="/app", pathname is "/bookmarks" not "/app/bookmarks").
@@ -61,6 +62,12 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
61
62
  ]
62
63
  : []),
63
64
  { pathForLink: `${adminBaseLink}/ai`, pathForActive: `${adminBaseFull}/ai`, label: t('admin.ai.nav'), icon: Sparkles },
65
+ ...(extraAdminNavItems ?? []).map(({ path, label }) => ({
66
+ pathForLink: `${adminBaseLink}/${path}`.replace(/\/+/g, '/'),
67
+ pathForActive: `${adminBaseFull}/${path}`.replace(/\/+/g, '/'),
68
+ label,
69
+ icon: CreditCard,
70
+ })),
64
71
  ];
65
72
 
66
73
  const rootActivePath = pathBaseForActive || '/';
@@ -19,6 +19,10 @@ export interface AppConfig {
19
19
  bookmarkLimit?: number | null;
20
20
  /** True only when plan is team. Used to gate team sharing UI. */
21
21
  canShareWithTeams?: boolean;
22
+ /** Optional extra admin nav items (e.g. cloud billing). Generic extension; no cloud-specific naming. */
23
+ extraAdminNavItems?: { path: string; label: string }[];
24
+ /** Optional extra admin route definitions (e.g. cloud billing). Generic extension. */
25
+ extraAdminRoutes?: { path: string; element: React.ReactNode }[];
22
26
  }
23
27
 
24
28
  const defaultConfig: AppConfig = {
@@ -44,6 +48,8 @@ export function AppConfigProvider({
44
48
  plan,
45
49
  bookmarkLimit,
46
50
  canShareWithTeams,
51
+ extraAdminNavItems,
52
+ extraAdminRoutes,
47
53
  }: {
48
54
  children: React.ReactNode;
49
55
  appBasePath?: string;
@@ -62,6 +68,10 @@ export function AppConfigProvider({
62
68
  bookmarkLimit?: number | null;
63
69
  /** True only when plan is team. */
64
70
  canShareWithTeams?: boolean;
71
+ /** Optional extra admin nav items (e.g. cloud billing). Generic extension. */
72
+ extraAdminNavItems?: { path: string; label: string }[];
73
+ /** Optional extra admin route definitions (e.g. cloud billing). Generic extension. */
74
+ extraAdminRoutes?: { path: string; element: React.ReactNode }[];
65
75
  }) {
66
76
  const base = appBasePath ?? defaultConfig.appBasePath;
67
77
  const value: AppConfig = {
@@ -75,6 +85,8 @@ export function AppConfigProvider({
75
85
  plan,
76
86
  bookmarkLimit,
77
87
  canShareWithTeams,
88
+ extraAdminNavItems,
89
+ extraAdminRoutes,
78
90
  };
79
91
  return <AppConfigContext.Provider value={value}>{children}</AppConfigContext.Provider>;
80
92
  }
@@ -635,6 +635,9 @@
635
635
  "searchEngineGuide": {
636
636
  "title": "Anleitung zur Einrichtung benutzerdefinierter Suchmaschinen",
637
637
  "description": "So richtest du eine Suchmaschine im Browser ein und greifst schnell auf deine Lesezeichen zu",
638
+ "tabChromium": "Chromium-basiert",
639
+ "tabFirefox": "Firefox",
640
+ "stepTitle": "Schritt {{n}}",
638
641
  "howItWorks": "Wie es funktioniert",
639
642
  "howItWorksDescription": "Mit einer Suchmaschine tippst du in der Adresszeile z. B. 'go' plus Slug und landest direkt beim Lesezeichen.",
640
643
  "yourSearchUrl": "Ihre Such-URL",
@@ -675,6 +675,9 @@
675
675
  "searchEngineGuide": {
676
676
  "title": "Custom Search Engine Setup Guide",
677
677
  "description": "Learn how to set up a custom search engine in your browser to quickly access your bookmarks",
678
+ "tabChromium": "Chromium-based",
679
+ "tabFirefox": "Firefox",
680
+ "stepTitle": "Step {{n}}",
678
681
  "howItWorks": "How It Works",
679
682
  "howItWorksDescription": "By setting up a custom search engine, you can type a keyword (like 'go') followed by a bookmark slug in your browser's address bar to instantly navigate to that bookmark.",
680
683
  "yourSearchUrl": "Your Search URL",
@@ -636,6 +636,9 @@
636
636
  "searchEngineGuide": {
637
637
  "title": "Guía de configuración de motor de búsqueda personalizado",
638
638
  "description": "Aprende cómo configurar un motor de búsqueda personalizado en tu navegador para acceder rápidamente a tus marcadores",
639
+ "tabChromium": "Basado en Chromium",
640
+ "tabFirefox": "Firefox",
641
+ "stepTitle": "Paso {{n}}",
639
642
  "howItWorks": "Cómo funciona",
640
643
  "howItWorksDescription": "Al configurar un motor de búsqueda personalizado, puedes escribir una palabra clave (como 'go') seguida de un slug de marcador en la barra de direcciones de tu navegador para navegar instantáneamente a ese marcador.",
641
644
  "yourSearchUrl": "Tu URL de búsqueda",
@@ -459,6 +459,9 @@
459
459
  "searchEngineGuide": {
460
460
  "title": "Guide de configuration du moteur de recherche personnalisé",
461
461
  "description": "Apprenez à configurer un moteur de recherche personnalisé dans votre navigateur pour accéder rapidement à vos favoris",
462
+ "tabChromium": "Basé sur Chromium",
463
+ "tabFirefox": "Firefox",
464
+ "stepTitle": "Étape {{n}}",
462
465
  "howItWorks": "Comment ça fonctionne",
463
466
  "howItWorksDescription": "En configurant un moteur de recherche personnalisé, vous pouvez taper un mot-clé (comme 'go') suivi d'un slug de favori dans la barre d'adresse de votre navigateur pour naviguer instantanément vers ce favori.",
464
467
  "yourSearchUrl": "Votre URL de recherche",
@@ -444,6 +444,9 @@
444
444
  "searchEngineGuide": {
445
445
  "title": "Guida alla configurazione del motore di ricerca personalizzato",
446
446
  "description": "Scopri come configurare un motore di ricerca personalizzato nel tuo browser per accedere rapidamente ai tuoi segnalibri",
447
+ "tabChromium": "Basato su Chromium",
448
+ "tabFirefox": "Firefox",
449
+ "stepTitle": "Passo {{n}}",
447
450
  "howItWorks": "Come funziona",
448
451
  "howItWorksDescription": "Configurando un motore di ricerca personalizzato, puoi digitare una parola chiave (come 'go') seguita da uno slug di segnalibro nella barra degli indirizzi del browser per navigare istantaneamente a quel segnalibro.",
449
452
  "yourSearchUrl": "La tua URL di ricerca",
@@ -444,6 +444,9 @@
444
444
  "searchEngineGuide": {
445
445
  "title": "カスタム検索エンジン設定ガイド",
446
446
  "description": "ブラウザでカスタム検索エンジンを設定してブックマークにすばやくアクセスする方法を学ぶ",
447
+ "tabChromium": "Chromiumベース",
448
+ "tabFirefox": "Firefox",
449
+ "stepTitle": "ステップ {{n}}",
447
450
  "howItWorks": "仕組み",
448
451
  "howItWorksDescription": "カスタム検索エンジンを設定すると、ブラウザのアドレスバーにキーワード(例:'go')に続いてブックマークスラッグを入力することで、そのブックマークに瞬時に移動できます。",
449
452
  "yourSearchUrl": "検索URL",
@@ -459,6 +459,9 @@
459
459
  "searchEngineGuide": {
460
460
  "title": "Gids voor aangepaste zoekmachine-instelling",
461
461
  "description": "Leer hoe je een aangepaste zoekmachine in je browser instelt om snel toegang te krijgen tot je bladwijzers",
462
+ "tabChromium": "Chromium-gebaseerd",
463
+ "tabFirefox": "Firefox",
464
+ "stepTitle": "Stap {{n}}",
462
465
  "howItWorks": "Hoe het werkt",
463
466
  "howItWorksDescription": "Door een aangepaste zoekmachine in te stellen, kun je een trefwoord (zoals 'go') gevolgd door een bladwijzerslug in de adresbalk van je browser typen om direct naar die bladwijzer te navigeren.",
464
467
  "yourSearchUrl": "Je zoek-URL",
@@ -444,6 +444,9 @@
444
444
  "searchEngineGuide": {
445
445
  "title": "Przewodnik konfiguracji niestandardowej wyszukiwarki",
446
446
  "description": "Dowiedz się, jak skonfigurować niestandardową wyszukiwarkę w przeglądarce, aby szybko uzyskać dostęp do zakładek",
447
+ "tabChromium": "Na bazie Chromium",
448
+ "tabFirefox": "Firefox",
449
+ "stepTitle": "Krok {{n}}",
447
450
  "howItWorks": "Jak to działa",
448
451
  "howItWorksDescription": "Konfigurując niestandardową wyszukiwarkę, możesz wpisać słowo kluczowe (np. 'go'), a następnie slug zakładki w pasku adresu przeglądarki, aby natychmiast przejść do tej zakładki.",
449
452
  "yourSearchUrl": "Twój URL wyszukiwania",
@@ -444,6 +444,9 @@
444
444
  "searchEngineGuide": {
445
445
  "title": "Guia de configuração de mecanismo de pesquisa personalizado",
446
446
  "description": "Aprenda como configurar um mecanismo de pesquisa personalizado no seu navegador para acessar rapidamente seus favoritos",
447
+ "tabChromium": "Baseado em Chromium",
448
+ "tabFirefox": "Firefox",
449
+ "stepTitle": "Passo {{n}}",
447
450
  "howItWorks": "Como funciona",
448
451
  "howItWorksDescription": "Ao configurar um mecanismo de pesquisa personalizado, você pode digitar uma palavra-chave (como 'go') seguida de um slug de favorito na barra de endereços do navegador para navegar instantaneamente para esse favorito.",
449
452
  "yourSearchUrl": "Sua URL de pesquisa",
@@ -444,6 +444,9 @@
444
444
  "searchEngineGuide": {
445
445
  "title": "Руководство по настройке пользовательской поисковой системы",
446
446
  "description": "Узнайте, как настроить пользовательскую поисковую систему в браузере для быстрого доступа к закладкам",
447
+ "tabChromium": "На базе Chromium",
448
+ "tabFirefox": "Firefox",
449
+ "stepTitle": "Шаг {{n}}",
447
450
  "howItWorks": "Как это работает",
448
451
  "howItWorksDescription": "Настроив пользовательскую поисковую систему, вы можете ввести ключевое слово (например, 'go'), за которым следует slug закладки в адресной строке браузера, чтобы мгновенно перейти к этой закладке.",
449
452
  "yourSearchUrl": "Ваш URL для поиска",
@@ -444,6 +444,9 @@
444
444
  "searchEngineGuide": {
445
445
  "title": "自定义搜索引擎设置指南",
446
446
  "description": "了解如何在浏览器中设置自定义搜索引擎以快速访问您的书签",
447
+ "tabChromium": "基于 Chromium",
448
+ "tabFirefox": "Firefox",
449
+ "stepTitle": "步骤 {{n}}",
447
450
  "howItWorks": "工作原理",
448
451
  "howItWorksDescription": "通过设置自定义搜索引擎,您可以在浏览器的地址栏中输入关键字(如'go'),后跟书签别名,即可立即导航到该书签。",
449
452
  "yourSearchUrl": "您的搜索URL",
@@ -246,9 +246,9 @@ export default function Dashboard() {
246
246
  usage: {
247
247
  used: stats.tenantBookmarkCount ?? stats.totalBookmarks,
248
248
  limit: stats.bookmarkLimit,
249
- labelOverride: t('sharing.bookmarksUsed', { count: stats.tenantBookmarkCount ?? stats.totalBookmarks, limit: stats.bookmarkLimit }),
249
+ labelOverride: t('plan.bookmarksUsed', { count: stats.tenantBookmarkCount ?? stats.totalBookmarks, limit: stats.bookmarkLimit }),
250
250
  showProgress: true,
251
- cta: (stats.tenantBookmarkCount ?? stats.totalBookmarks) >= stats.bookmarkLimit ? { label: t('sharing.limitBookmarks', { limit: stats.bookmarkLimit }), onClick: () => window.location.href = '/pricing' } : undefined,
251
+ cta: (stats.tenantBookmarkCount ?? stats.totalBookmarks) >= stats.bookmarkLimit ? { label: t('plan.limitBookmarks', { limit: stats.bookmarkLimit }), onClick: () => window.location.href = '/pricing' } : undefined,
252
252
  },
253
253
  }),
254
254
  }}
@@ -1,136 +1,239 @@
1
+ import { useState } from 'react';
1
2
  import { useTranslation } from 'react-i18next';
2
- import { ArrowLeft, Search, Code, CheckCircle } from 'lucide-react';
3
+ import { ArrowLeft, Code } from 'lucide-react';
3
4
  import { Link } from 'react-router-dom';
4
5
  import Button from '../components/ui/Button';
6
+ import { PageHeader } from '../components/PageHeader';
7
+ import { ScopeSegmentedControl } from '../components/ScopeSegmentedControl';
8
+ import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
5
9
  import { useAppConfig } from '../contexts/AppConfigContext';
6
10
 
7
11
  export default function SearchEngineGuide() {
8
12
  const { t } = useTranslation();
9
13
  const { pathPrefixForLinks } = useAppConfig();
10
14
  const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
15
+ const [activeTab, setActiveTab] = useState<'chromium' | 'firefox'>('chromium');
11
16
 
12
17
  const baseUrl = window.location.origin;
13
18
  const goPath = '/go/%s';
14
19
  const searchUrl = `${baseUrl}${goPath}`;
15
20
 
21
+ const tabOptions = [
22
+ { value: 'chromium', label: t('searchEngineGuide.tabChromium') },
23
+ { value: 'firefox', label: t('searchEngineGuide.tabFirefox') },
24
+ ];
25
+
16
26
  return (
17
27
  <div className="space-y-6 max-w-4xl mx-auto">
18
- {/* Header */}
19
- <div className="flex items-center gap-4">
28
+ {/* Header: back + PageHeader with tabs */}
29
+ <div className="flex flex-col gap-4">
20
30
  <Link to={`${prefix}/bookmarks`}>
21
31
  <Button variant="ghost" size="sm" icon={ArrowLeft}>
22
32
  {t('common.back')}
23
33
  </Button>
24
34
  </Link>
25
- <div>
26
- <h1 className="text-3xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
27
- <Search className="h-8 w-8" />
28
- {t('searchEngineGuide.title')}
29
- </h1>
30
- <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
31
- {t('searchEngineGuide.description')}
32
- </p>
33
- </div>
35
+ <PageHeader
36
+ title={t('searchEngineGuide.title')}
37
+ subtitle={t('searchEngineGuide.description')}
38
+ actions={
39
+ <ScopeSegmentedControl
40
+ value={activeTab}
41
+ onChange={(v) => setActiveTab(v as 'chromium' | 'firefox')}
42
+ options={tabOptions}
43
+ ariaLabel={t('searchEngineGuide.title')}
44
+ />
45
+ }
46
+ />
34
47
  </div>
35
48
 
36
49
  {/* How it works */}
37
- <div className="bg-primary/10 border border-primary/30 rounded-xl p-6">
38
- <h2 className="text-xl font-semibold text-foreground mb-3">
39
- {t('searchEngineGuide.howItWorks')}
40
- </h2>
41
- <p className="text-muted-foreground mb-4">
42
- {t('searchEngineGuide.howItWorksDescription')}
43
- </p>
44
- <div className="bg-card rounded-lg p-4 border border-primary/30">
45
- <div className="flex items-center gap-2 text-sm font-mono text-foreground">
46
- <Code className="h-4 w-4 text-primary" />
47
- <span className="text-primary">go</span>
48
- <span className="text-gray-400">your-slug</span>
50
+ <Card className="bg-primary/10 border-primary/30">
51
+ <CardHeader>
52
+ <CardTitle className="text-xl text-foreground">
53
+ {t('searchEngineGuide.howItWorks')}
54
+ </CardTitle>
55
+ </CardHeader>
56
+ <CardContent className="space-y-4">
57
+ <p className="text-muted-foreground">
58
+ {t('searchEngineGuide.howItWorksDescription')}
59
+ </p>
60
+ <div className="bg-card rounded-lg p-4 border border-border">
61
+ <div className="flex items-center gap-2 text-sm font-mono text-foreground">
62
+ <Code className="h-4 w-4 text-primary" />
63
+ <span className="text-primary">go</span>
64
+ <span className="text-muted-foreground">your-slug</span>
65
+ </div>
49
66
  </div>
50
- </div>
51
- </div>
67
+ </CardContent>
68
+ </Card>
52
69
 
53
70
  {/* Your search URL */}
54
- <div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
55
- <h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
56
- {t('searchEngineGuide.yourSearchUrl')}
57
- </h2>
58
- <div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
59
- <code className="text-sm font-mono text-gray-900 dark:text-white break-all">
60
- {searchUrl}
61
- </code>
62
- </div>
63
- <p className="mt-3 text-sm text-gray-600 dark:text-gray-400">
64
- {t('searchEngineGuide.urlNote')}
65
- </p>
66
- </div>
71
+ <Card>
72
+ <CardHeader>
73
+ <CardTitle className="text-lg">{t('searchEngineGuide.yourSearchUrl')}</CardTitle>
74
+ </CardHeader>
75
+ <CardContent className="space-y-3">
76
+ <div className="bg-muted rounded-lg p-4 border border-border">
77
+ <code className="text-sm font-mono text-foreground break-all">{searchUrl}</code>
78
+ </div>
79
+ <p className="text-sm text-muted-foreground">{t('searchEngineGuide.urlNote')}</p>
80
+ </CardContent>
81
+ </Card>
67
82
 
68
- {/* Chromium-based browsers */}
69
- <div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
70
- <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
71
- <CheckCircle className="h-6 w-6 text-green-600 dark:text-green-400" />
72
- {t('searchEngineGuide.chromiumTitle')}
73
- </h2>
74
- <p className="text-gray-700 dark:text-gray-300 mb-4">
75
- {t('searchEngineGuide.chromiumDescription')}
76
- </p>
77
- <ol className="list-decimal list-inside space-y-3 text-gray-700 dark:text-gray-300">
78
- <li>{t('searchEngineGuide.chromiumStep1')}</li>
79
- <li>{t('searchEngineGuide.chromiumStep2')}</li>
80
- <li>{t('searchEngineGuide.chromiumStep3')}</li>
81
- <li>
82
- {t('searchEngineGuide.chromiumStep4')}
83
- <ul className="list-disc list-inside ml-6 mt-2 space-y-1 text-sm text-gray-600 dark:text-gray-400">
84
- <li>{t('searchEngineGuide.chromiumStep4a')}</li>
85
- <li>{t('searchEngineGuide.chromiumStep4b')}</li>
86
- <li>{t('searchEngineGuide.chromiumStep4c')}</li>
87
- </ul>
88
- </li>
89
- <li>{t('searchEngineGuide.chromiumStep5')}</li>
90
- </ol>
91
- </div>
83
+ {/* Chromium steps (only when active) */}
84
+ {activeTab === 'chromium' && (
85
+ <div className="space-y-4">
86
+ <h2 className="text-lg font-semibold text-foreground">
87
+ {t('searchEngineGuide.chromiumTitle')}
88
+ </h2>
89
+ <p className="text-muted-foreground text-sm">
90
+ {t('searchEngineGuide.chromiumDescription')}
91
+ </p>
92
+ <div className="space-y-4">
93
+ <Card>
94
+ <CardHeader className="pb-2">
95
+ <CardTitle className="text-base">
96
+ {t('searchEngineGuide.stepTitle', { n: 1 })}
97
+ </CardTitle>
98
+ </CardHeader>
99
+ <CardContent>
100
+ <p className="text-foreground">{t('searchEngineGuide.chromiumStep1')}</p>
101
+ </CardContent>
102
+ </Card>
103
+ <Card>
104
+ <CardHeader className="pb-2">
105
+ <CardTitle className="text-base">
106
+ {t('searchEngineGuide.stepTitle', { n: 2 })}
107
+ </CardTitle>
108
+ </CardHeader>
109
+ <CardContent>
110
+ <p className="text-foreground">{t('searchEngineGuide.chromiumStep2')}</p>
111
+ </CardContent>
112
+ </Card>
113
+ <Card>
114
+ <CardHeader className="pb-2">
115
+ <CardTitle className="text-base">
116
+ {t('searchEngineGuide.stepTitle', { n: 3 })}
117
+ </CardTitle>
118
+ </CardHeader>
119
+ <CardContent>
120
+ <p className="text-foreground">{t('searchEngineGuide.chromiumStep3')}</p>
121
+ </CardContent>
122
+ </Card>
123
+ <Card>
124
+ <CardHeader className="pb-2">
125
+ <CardTitle className="text-base">
126
+ {t('searchEngineGuide.stepTitle', { n: 4 })}
127
+ </CardTitle>
128
+ </CardHeader>
129
+ <CardContent className="space-y-2">
130
+ <p className="text-foreground">{t('searchEngineGuide.chromiumStep4')}</p>
131
+ <ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground ml-2">
132
+ <li>{t('searchEngineGuide.chromiumStep4a')}</li>
133
+ <li>{t('searchEngineGuide.chromiumStep4b')}</li>
134
+ <li>{t('searchEngineGuide.chromiumStep4c')}</li>
135
+ </ul>
136
+ </CardContent>
137
+ </Card>
138
+ <Card>
139
+ <CardHeader className="pb-2">
140
+ <CardTitle className="text-base">
141
+ {t('searchEngineGuide.stepTitle', { n: 5 })}
142
+ </CardTitle>
143
+ </CardHeader>
144
+ <CardContent>
145
+ <p className="text-foreground">{t('searchEngineGuide.chromiumStep5')}</p>
146
+ </CardContent>
147
+ </Card>
148
+ </div>
149
+ </div>
150
+ )}
92
151
 
93
- {/* Firefox */}
94
- <div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
95
- <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
96
- <CheckCircle className="h-6 w-6 text-orange-600 dark:text-orange-400" />
97
- {t('searchEngineGuide.firefoxTitle')}
98
- </h2>
99
- <p className="text-gray-700 dark:text-gray-300 mb-4">
100
- {t('searchEngineGuide.firefoxDescription')}
101
- </p>
102
- <ol className="list-decimal list-inside space-y-3 text-gray-700 dark:text-gray-300">
103
- <li>{t('searchEngineGuide.firefoxStep1')}</li>
104
- <li>{t('searchEngineGuide.firefoxStep2')}</li>
105
- <li>{t('searchEngineGuide.firefoxStep3')}</li>
106
- <li>
107
- {t('searchEngineGuide.firefoxStep4')}
108
- <ul className="list-disc list-inside ml-6 mt-2 space-y-1 text-sm text-gray-600 dark:text-gray-400">
109
- <li>{t('searchEngineGuide.firefoxStep4a')}</li>
110
- <li>{t('searchEngineGuide.firefoxStep4b')}</li>
111
- <li>{t('searchEngineGuide.firefoxStep4c')}</li>
112
- </ul>
113
- </li>
114
- <li>{t('searchEngineGuide.firefoxStep5')}</li>
115
- </ol>
116
- </div>
152
+ {/* Firefox steps (only when active) */}
153
+ {activeTab === 'firefox' && (
154
+ <div className="space-y-4">
155
+ <h2 className="text-lg font-semibold text-foreground">
156
+ {t('searchEngineGuide.firefoxTitle')}
157
+ </h2>
158
+ <p className="text-muted-foreground text-sm">
159
+ {t('searchEngineGuide.firefoxDescription')}
160
+ </p>
161
+ <div className="space-y-4">
162
+ <Card>
163
+ <CardHeader className="pb-2">
164
+ <CardTitle className="text-base">
165
+ {t('searchEngineGuide.stepTitle', { n: 1 })}
166
+ </CardTitle>
167
+ </CardHeader>
168
+ <CardContent>
169
+ <p className="text-foreground">{t('searchEngineGuide.firefoxStep1')}</p>
170
+ </CardContent>
171
+ </Card>
172
+ <Card>
173
+ <CardHeader className="pb-2">
174
+ <CardTitle className="text-base">
175
+ {t('searchEngineGuide.stepTitle', { n: 2 })}
176
+ </CardTitle>
177
+ </CardHeader>
178
+ <CardContent>
179
+ <p className="text-foreground">{t('searchEngineGuide.firefoxStep2')}</p>
180
+ </CardContent>
181
+ </Card>
182
+ <Card>
183
+ <CardHeader className="pb-2">
184
+ <CardTitle className="text-base">
185
+ {t('searchEngineGuide.stepTitle', { n: 3 })}
186
+ </CardTitle>
187
+ </CardHeader>
188
+ <CardContent>
189
+ <p className="text-foreground">{t('searchEngineGuide.firefoxStep3')}</p>
190
+ </CardContent>
191
+ </Card>
192
+ <Card>
193
+ <CardHeader className="pb-2">
194
+ <CardTitle className="text-base">
195
+ {t('searchEngineGuide.stepTitle', { n: 4 })}
196
+ </CardTitle>
197
+ </CardHeader>
198
+ <CardContent className="space-y-2">
199
+ <p className="text-foreground">{t('searchEngineGuide.firefoxStep4')}</p>
200
+ <ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground ml-2">
201
+ <li>{t('searchEngineGuide.firefoxStep4a')}</li>
202
+ <li>{t('searchEngineGuide.firefoxStep4b')}</li>
203
+ <li>{t('searchEngineGuide.firefoxStep4c')}</li>
204
+ </ul>
205
+ </CardContent>
206
+ </Card>
207
+ <Card>
208
+ <CardHeader className="pb-2">
209
+ <CardTitle className="text-base">
210
+ {t('searchEngineGuide.stepTitle', { n: 5 })}
211
+ </CardTitle>
212
+ </CardHeader>
213
+ <CardContent>
214
+ <p className="text-foreground">{t('searchEngineGuide.firefoxStep5')}</p>
215
+ </CardContent>
216
+ </Card>
217
+ </div>
218
+ </div>
219
+ )}
117
220
 
118
221
  {/* Usage example */}
119
- <div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl p-6">
120
- <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
121
- {t('searchEngineGuide.usageExample')}
122
- </h2>
123
- <div className="space-y-2 text-gray-700 dark:text-gray-300">
124
- <p>{t('searchEngineGuide.usageStep1')}</p>
125
- <div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-green-200 dark:border-green-700">
126
- <code className="text-sm font-mono text-gray-900 dark:text-white">go test</code>
222
+ <Card className="bg-primary/10 border-primary/30">
223
+ <CardHeader>
224
+ <CardTitle className="text-xl text-foreground">
225
+ {t('searchEngineGuide.usageExample')}
226
+ </CardTitle>
227
+ </CardHeader>
228
+ <CardContent className="space-y-3">
229
+ <p className="text-foreground">{t('searchEngineGuide.usageStep1')}</p>
230
+ <div className="bg-card rounded-lg p-3 border border-border">
231
+ <code className="text-sm font-mono text-foreground">go test</code>
127
232
  </div>
128
- <p className="mt-3">{t('searchEngineGuide.usageStep2')}</p>
129
- <p className="text-sm text-gray-600 dark:text-gray-400">
130
- {t('searchEngineGuide.usageNote')}
131
- </p>
132
- </div>
133
- </div>
233
+ <p className="text-foreground">{t('searchEngineGuide.usageStep2')}</p>
234
+ <p className="text-sm text-muted-foreground">{t('searchEngineGuide.usageNote')}</p>
235
+ </CardContent>
236
+ </Card>
134
237
  </div>
135
238
  );
136
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdguggenbichler/slugbase-core",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "description": "SlugBase core: backend and frontend entrypoints for self-hosted and cloud apps",
5
5
  "type": "module",
6
6
  "exports": {