@brainfish-ai/devdoc 0.1.29 → 0.1.31

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.
@@ -150,10 +150,10 @@ export function DocsSidebar({
150
150
 
151
151
  return (
152
152
  <>
153
- {/* Mobile overlay backdrop */}
153
+ {/* Mobile overlay backdrop - z-[55] to cover header (z-50) but below sidebar (z-60) */}
154
154
  {isMobile && isOpen && (
155
155
  <div
156
- className="fixed inset-0 bg-black/50 z-40 lg:hidden"
156
+ className="fixed inset-0 bg-black/50 z-[55] lg:hidden"
157
157
  onClick={onClose}
158
158
  />
159
159
  )}
@@ -162,9 +162,9 @@ export function DocsSidebar({
162
162
  className={cn(
163
163
  "flex flex-col border-r bg-sidebar border-sidebar-border overflow-hidden",
164
164
  // Desktop: always visible, fixed width
165
- "lg:relative lg:w-72 lg:h-full",
166
- // Mobile: drawer behavior
167
- "fixed inset-y-0 left-0 z-50 w-[280px] h-full",
165
+ "lg:relative lg:w-72 lg:h-full lg:z-auto",
166
+ // Mobile: drawer behavior - z-[60] to appear above header (z-50)
167
+ "fixed inset-y-0 left-0 z-[60] w-[280px] h-full",
168
168
  "transform transition-transform duration-300 ease-in-out",
169
169
  "lg:transform-none lg:translate-x-0",
170
170
  isMobile && !isOpen && "-translate-x-full",
@@ -25,6 +25,7 @@ interface DocsLogo {
25
25
  height?: number
26
26
  light?: string
27
27
  dark?: string
28
+ href?: string // Where logo links to (defaults to "/")
28
29
  }
29
30
 
30
31
  // Header config from docs.json
@@ -133,7 +134,10 @@ export function DocsHeader({
133
134
  </Button>
134
135
  )}
135
136
 
136
- <Link href="/" className="docs-header-logo flex items-center gap-2 sm:gap-3">
137
+ <Link
138
+ href={docsLogo?.href || "/"}
139
+ className="docs-header-logo flex items-center gap-2 sm:gap-3 min-h-[44px] min-w-[44px] touch-manipulation"
140
+ >
137
141
  {hasLightDarkLogos ? (
138
142
  <>
139
143
  {/* Light mode logo */}
@@ -142,7 +146,7 @@ export function DocsHeader({
142
146
  alt={logo.alt}
143
147
  width={logo.width}
144
148
  height={logo.height}
145
- className="w-auto dark:hidden"
149
+ className="w-auto dark:hidden pointer-events-none"
146
150
  style={{ height: logo.height }}
147
151
  priority
148
152
  />
@@ -152,7 +156,7 @@ export function DocsHeader({
152
156
  alt={logo.alt}
153
157
  width={logo.width}
154
158
  height={logo.height}
155
- className="w-auto hidden dark:block"
159
+ className="w-auto hidden dark:block pointer-events-none"
156
160
  style={{ height: logo.height }}
157
161
  priority
158
162
  />
@@ -163,7 +167,7 @@ export function DocsHeader({
163
167
  alt={logo.alt}
164
168
  width={logo.width}
165
169
  height={logo.height}
166
- className="w-auto dark:invert"
170
+ className="w-auto dark:invert pointer-events-none"
167
171
  style={{ height: logo.height }}
168
172
  priority
169
173
  />
@@ -171,7 +175,7 @@ export function DocsHeader({
171
175
  {docsName && (
172
176
  <>
173
177
  <div className="hidden sm:block h-5 w-px bg-border" />
174
- <span className="docs-header-title hidden sm:inline text-sm font-medium text-muted-foreground">
178
+ <span className="docs-header-title hidden sm:inline text-sm font-medium text-muted-foreground pointer-events-none">
175
179
  {docsName}
176
180
  </span>
177
181
  </>
@@ -107,10 +107,10 @@ export function RightSidebar({
107
107
 
108
108
  return (
109
109
  <>
110
- {/* Mobile overlay backdrop */}
110
+ {/* Mobile overlay backdrop - z-[55] to cover header (z-50) but below panel (z-60) */}
111
111
  {isMobile && isRightSidebarOpen && (
112
112
  <div
113
- className="docs-agent-overlay fixed inset-0 bg-black/50 z-40 lg:hidden"
113
+ className="docs-agent-overlay fixed inset-0 bg-black/50 z-[55] lg:hidden"
114
114
  onClick={closeRightSidebar}
115
115
  />
116
116
  )}
@@ -119,9 +119,9 @@ export function RightSidebar({
119
119
  className={cn(
120
120
  "docs-agent-panel border-l border-border bg-background flex flex-col overflow-hidden",
121
121
  // Desktop: always visible, fixed width
122
- "lg:relative lg:w-96 lg:h-full",
123
- // Mobile: drawer behavior from right
124
- "fixed inset-y-0 right-0 z-50 w-[320px] sm:w-[360px] h-full",
122
+ "lg:relative lg:w-96 lg:h-full lg:z-auto",
123
+ // Mobile: drawer behavior from right - z-[60] to appear above header (z-50)
124
+ "fixed inset-y-0 right-0 z-[60] w-[320px] sm:w-[360px] h-full",
125
125
  "transform transition-transform duration-300 ease-in-out",
126
126
  "lg:transform-none lg:translate-x-0",
127
127
  isMobile && !isRightSidebarOpen && "translate-x-full",
@@ -0,0 +1,260 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Domain Configuration Schema (domain.json)
5
+ *
6
+ * Configuration for custom domains on DevDoc projects.
7
+ * Each project can have ONE custom domain for free.
8
+ */
9
+
10
+ // ============================================================================
11
+ // Domain Validation Helpers
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Valid domain pattern - allows subdomains like docs.example.com
16
+ * Does NOT allow:
17
+ * - IP addresses
18
+ * - Ports
19
+ * - Paths
20
+ * - Protocol prefixes
21
+ */
22
+ const domainRegex = /^(?!-)[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$/
23
+
24
+ /**
25
+ * Reserved domains that cannot be used as custom domains
26
+ */
27
+ const RESERVED_DOMAINS = [
28
+ 'devdoc.sh',
29
+ 'devdoc.io',
30
+ 'devdoc.com',
31
+ 'localhost',
32
+ ]
33
+
34
+ // ============================================================================
35
+ // SEO Schema
36
+ // ============================================================================
37
+
38
+ const domainSeoSchema = z.object({
39
+ /**
40
+ * Canonical URL for SEO - tells search engines this is the primary URL
41
+ * Should match your custom domain with https://
42
+ */
43
+ canonical: z.string().url().optional(),
44
+ })
45
+
46
+ // ============================================================================
47
+ // Settings Schema
48
+ // ============================================================================
49
+
50
+ const domainSettingsSchema = z.object({
51
+ /**
52
+ * Force HTTPS redirects (recommended, default: true)
53
+ */
54
+ forceHttps: z.boolean().default(true),
55
+
56
+ /**
57
+ * WWW redirect preference
58
+ * - "www": Redirect non-www to www (example.com → www.example.com)
59
+ * - "non-www": Redirect www to non-www (www.example.com → example.com)
60
+ * - "none": No redirect, accept both
61
+ */
62
+ wwwRedirect: z.enum(['www', 'non-www', 'none']).default('none'),
63
+ })
64
+
65
+ // ============================================================================
66
+ // Main Domain Configuration Schema
67
+ // ============================================================================
68
+
69
+ export const domainConfigSchema = z.object({
70
+ /**
71
+ * JSON Schema reference (optional)
72
+ */
73
+ $schema: z.string().optional(),
74
+
75
+ /**
76
+ * The custom domain for this project
77
+ * Example: "docs.example.com" or "developer.mycompany.io"
78
+ *
79
+ * Rules:
80
+ * - Must be a valid domain name
81
+ * - Cannot be a DevDoc domain (*.devdoc.sh)
82
+ * - One custom domain per project (free)
83
+ */
84
+ customDomain: z.string()
85
+ .min(4, 'Domain must be at least 4 characters')
86
+ .max(253, 'Domain must be 253 characters or less')
87
+ .regex(domainRegex, 'Invalid domain format. Example: docs.example.com')
88
+ .refine(
89
+ (domain) => !RESERVED_DOMAINS.some(reserved =>
90
+ domain === reserved || domain.endsWith(`.${reserved}`)
91
+ ),
92
+ 'Cannot use a reserved DevDoc domain'
93
+ ),
94
+
95
+ /**
96
+ * SEO configuration for the custom domain
97
+ */
98
+ seo: domainSeoSchema.optional(),
99
+
100
+ /**
101
+ * Domain behavior settings
102
+ */
103
+ settings: domainSettingsSchema.optional(),
104
+ })
105
+
106
+ // ============================================================================
107
+ // Types
108
+ // ============================================================================
109
+
110
+ export type DomainConfig = z.infer<typeof domainConfigSchema>
111
+ export type DomainSeoConfig = z.infer<typeof domainSeoSchema>
112
+ export type DomainSettings = z.infer<typeof domainSettingsSchema>
113
+
114
+ // ============================================================================
115
+ // Domain Status Types (for API/storage)
116
+ // ============================================================================
117
+
118
+ /**
119
+ * Status of a custom domain in the system
120
+ */
121
+ export type DomainStatus =
122
+ | 'pending' // Domain added, awaiting DNS configuration
123
+ | 'dns_verified' // DNS records verified, awaiting SSL
124
+ | 'ssl_provisioning' // SSL certificate being provisioned
125
+ | 'active' // Domain is live and working
126
+ | 'error' // Configuration error
127
+
128
+ /**
129
+ * Custom domain entry stored in the registry
130
+ */
131
+ export interface CustomDomainEntry {
132
+ /** The custom domain (e.g., "docs.example.com") */
133
+ customDomain: string
134
+
135
+ /** The project slug this domain points to */
136
+ projectSlug: string
137
+
138
+ /** Current status of the domain */
139
+ status: DomainStatus
140
+
141
+ /** Vercel domain ID (from Vercel API) */
142
+ vercelDomainId?: string
143
+
144
+ /** TXT record value for domain ownership verification */
145
+ verificationToken?: string
146
+
147
+ /** When the domain was added */
148
+ createdAt: string
149
+
150
+ /** When DNS was verified */
151
+ verifiedAt?: string
152
+
153
+ /** Last time status was checked */
154
+ lastCheckedAt?: string
155
+
156
+ /** Error message if status is "error" */
157
+ errorMessage?: string
158
+
159
+ /** Domain settings from domain.json */
160
+ settings?: DomainSettings
161
+ }
162
+
163
+ // ============================================================================
164
+ // Validation Functions
165
+ // ============================================================================
166
+
167
+ /**
168
+ * Parse and validate domain configuration
169
+ */
170
+ export function parseDomainConfig(data: unknown): DomainConfig {
171
+ const result = domainConfigSchema.safeParse(data)
172
+
173
+ if (!result.success) {
174
+ const issues = result.error.issues || []
175
+ const errors = issues.map((e) =>
176
+ `${e.path.map(String).join('.')}: ${e.message}`
177
+ ).join('\n')
178
+ throw new Error(`Invalid domain.json configuration:\n${errors}`)
179
+ }
180
+
181
+ return result.data
182
+ }
183
+
184
+ /**
185
+ * Safe parse that returns null on failure
186
+ */
187
+ export function safeParseDomainConfig(data: unknown): DomainConfig | null {
188
+ const result = domainConfigSchema.safeParse(data)
189
+ return result.success ? result.data : null
190
+ }
191
+
192
+ /**
193
+ * Validate a domain string format
194
+ */
195
+ export function isValidDomain(domain: string): { valid: boolean; error?: string } {
196
+ if (!domain) {
197
+ return { valid: false, error: 'Domain is required' }
198
+ }
199
+
200
+ if (domain.length < 4) {
201
+ return { valid: false, error: 'Domain must be at least 4 characters' }
202
+ }
203
+
204
+ if (domain.length > 253) {
205
+ return { valid: false, error: 'Domain must be 253 characters or less' }
206
+ }
207
+
208
+ if (!domainRegex.test(domain)) {
209
+ return { valid: false, error: 'Invalid domain format. Example: docs.example.com' }
210
+ }
211
+
212
+ // Check reserved domains
213
+ if (RESERVED_DOMAINS.some(reserved =>
214
+ domain === reserved || domain.endsWith(`.${reserved}`)
215
+ )) {
216
+ return { valid: false, error: 'Cannot use a reserved DevDoc domain' }
217
+ }
218
+
219
+ return { valid: true }
220
+ }
221
+
222
+ /**
223
+ * Normalize a domain (lowercase, trim, remove protocol/path)
224
+ */
225
+ export function normalizeDomain(domain: string): string {
226
+ let normalized = domain.toLowerCase().trim()
227
+
228
+ // Remove protocol if present
229
+ normalized = normalized.replace(/^https?:\/\//, '')
230
+
231
+ // Remove path if present
232
+ normalized = normalized.split('/')[0]
233
+
234
+ // Remove port if present
235
+ normalized = normalized.split(':')[0]
236
+
237
+ return normalized
238
+ }
239
+
240
+ /**
241
+ * Get DNS instructions for a custom domain
242
+ */
243
+ export function getDnsInstructions(customDomain: string): {
244
+ cname: { name: string; value: string }
245
+ txt: { name: string; value: string }
246
+ } {
247
+ const parts = customDomain.split('.')
248
+ const subdomain = parts.length > 2 ? parts[0] : '@'
249
+
250
+ return {
251
+ cname: {
252
+ name: subdomain === '@' ? customDomain : subdomain,
253
+ value: 'cname.devdoc-dns.com',
254
+ },
255
+ txt: {
256
+ name: `_devdoc-verify.${customDomain}`,
257
+ value: '', // Will be filled with actual verification token
258
+ },
259
+ }
260
+ }
@@ -17,6 +17,20 @@ export {
17
17
  type ApiConfig,
18
18
  } from './schema'
19
19
 
20
+ export {
21
+ domainConfigSchema,
22
+ parseDomainConfig,
23
+ safeParseDomainConfig,
24
+ isValidDomain,
25
+ normalizeDomain,
26
+ getDnsInstructions,
27
+ type DomainConfig,
28
+ type DomainSeoConfig,
29
+ type DomainSettings,
30
+ type DomainStatus,
31
+ type CustomDomainEntry,
32
+ } from './domain-schema'
33
+
20
34
  export {
21
35
  loadDocsConfig,
22
36
  safeLoadDocsConfig,
@@ -415,15 +415,59 @@ function getApiKeyBlobPath(slug: string): string {
415
415
  }
416
416
 
417
417
  // =============================================================================
418
- // Domain Registry - O(1) lookups for subdomains and API keys
418
+ // Domain Registry - O(1) lookups for subdomains, API keys, and custom domains
419
419
  // =============================================================================
420
420
 
421
+ /**
422
+ * Status of a custom domain in the system
423
+ */
424
+ export type CustomDomainStatus =
425
+ | 'pending' // Domain added, awaiting DNS configuration
426
+ | 'dns_verified' // DNS records verified, awaiting SSL
427
+ | 'ssl_provisioning' // SSL certificate being provisioned
428
+ | 'active' // Domain is live and working
429
+ | 'error' // Configuration error
430
+
431
+ /**
432
+ * Custom domain entry stored in the registry
433
+ */
434
+ export interface CustomDomainEntry {
435
+ /** The custom domain (e.g., "docs.example.com") */
436
+ customDomain: string
437
+
438
+ /** The project slug this domain points to */
439
+ projectSlug: string
440
+
441
+ /** Current status of the domain */
442
+ status: CustomDomainStatus
443
+
444
+ /** Vercel domain ID (from Vercel API) */
445
+ vercelDomainId?: string
446
+
447
+ /** TXT record value for domain ownership verification */
448
+ verificationToken?: string
449
+
450
+ /** When the domain was added */
451
+ createdAt: string
452
+
453
+ /** When DNS was verified */
454
+ verifiedAt?: string
455
+
456
+ /** Last time status was checked */
457
+ lastCheckedAt?: string
458
+
459
+ /** Error message if status is "error" */
460
+ errorMessage?: string
461
+ }
462
+
421
463
  /**
422
464
  * Registry structure - single file for all domains/projects
423
465
  */
424
466
  export interface DomainRegistry {
425
- domains: Record<string, DomainEntry> // subdomain -> entry
426
- apiKeys: Record<string, string> // apiKey -> subdomain (for O(1) key validation)
467
+ domains: Record<string, DomainEntry> // subdomain -> entry
468
+ customDomains: Record<string, CustomDomainEntry> // customDomain -> entry (NEW)
469
+ projectToCustomDomain: Record<string, string> // projectSlug -> customDomain (reverse lookup)
470
+ apiKeys: Record<string, string> // apiKey -> subdomain (for O(1) key validation)
427
471
  updatedAt: string
428
472
  }
429
473
 
@@ -477,7 +521,13 @@ async function getRegistry(): Promise<DomainRegistry> {
477
521
  }
478
522
 
479
523
  // Return empty registry if not found
480
- return { domains: {}, apiKeys: {}, updatedAt: new Date().toISOString() }
524
+ return {
525
+ domains: {},
526
+ customDomains: {},
527
+ projectToCustomDomain: {},
528
+ apiKeys: {},
529
+ updatedAt: new Date().toISOString()
530
+ }
481
531
  }
482
532
 
483
533
  /**
@@ -555,6 +605,194 @@ export async function getDomainEntry(subdomain: string): Promise<DomainEntry | n
555
605
  return registry.domains[subdomain] || null
556
606
  }
557
607
 
608
+ // =============================================================================
609
+ // Custom Domain Management - One custom domain per project (free)
610
+ // =============================================================================
611
+
612
+ /**
613
+ * Generate a verification token for domain ownership
614
+ */
615
+ export function generateVerificationToken(): string {
616
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
617
+ let token = 'devdoc-verify='
618
+ for (let i = 0; i < 24; i++) {
619
+ token += chars.charAt(Math.floor(Math.random() * chars.length))
620
+ }
621
+ return token
622
+ }
623
+
624
+ /**
625
+ * Check if a custom domain is already registered (O(1) lookup)
626
+ */
627
+ export async function isCustomDomainRegistered(customDomain: string): Promise<boolean> {
628
+ const registry = await getRegistry()
629
+ return customDomain in (registry.customDomains || {})
630
+ }
631
+
632
+ /**
633
+ * Check if a project already has a custom domain (O(1) lookup)
634
+ */
635
+ export async function getProjectCustomDomain(projectSlug: string): Promise<CustomDomainEntry | null> {
636
+ const registry = await getRegistry()
637
+ const customDomain = registry.projectToCustomDomain?.[projectSlug]
638
+ if (!customDomain) return null
639
+ return registry.customDomains?.[customDomain] || null
640
+ }
641
+
642
+ /**
643
+ * Get custom domain entry by domain name (O(1) lookup)
644
+ */
645
+ export async function getCustomDomainEntry(customDomain: string): Promise<CustomDomainEntry | null> {
646
+ const registry = await getRegistry()
647
+ return registry.customDomains?.[customDomain] || null
648
+ }
649
+
650
+ /**
651
+ * Look up project slug from custom domain (O(1) lookup)
652
+ * Used by middleware for routing
653
+ */
654
+ export async function lookupCustomDomain(customDomain: string): Promise<CustomDomainEntry | null> {
655
+ const registry = await getRegistry()
656
+ const entry = registry.customDomains?.[customDomain]
657
+
658
+ // Only return if domain is active
659
+ if (entry && entry.status === 'active') {
660
+ return entry
661
+ }
662
+
663
+ return null
664
+ }
665
+
666
+ /**
667
+ * Add a custom domain to a project
668
+ * Returns error if project already has a custom domain (limit: 1 per project)
669
+ */
670
+ export async function addCustomDomain(
671
+ projectSlug: string,
672
+ customDomain: string
673
+ ): Promise<{ success: boolean; entry?: CustomDomainEntry; error?: string }> {
674
+ const registry = await getRegistry()
675
+
676
+ // Initialize custom domain maps if they don't exist
677
+ if (!registry.customDomains) {
678
+ registry.customDomains = {}
679
+ }
680
+ if (!registry.projectToCustomDomain) {
681
+ registry.projectToCustomDomain = {}
682
+ }
683
+
684
+ // Check if project already has a custom domain (limit: 1 per project)
685
+ const existingDomain = registry.projectToCustomDomain[projectSlug]
686
+ if (existingDomain) {
687
+ return {
688
+ success: false,
689
+ error: `Project already has a custom domain: ${existingDomain}. Remove it first before adding a new one.`,
690
+ }
691
+ }
692
+
693
+ // Check if this domain is already registered to another project
694
+ if (registry.customDomains[customDomain]) {
695
+ return {
696
+ success: false,
697
+ error: `Domain ${customDomain} is already registered to another project.`,
698
+ }
699
+ }
700
+
701
+ // Verify the project exists
702
+ const projectExists = await getDomainEntry(projectSlug)
703
+ if (!projectExists) {
704
+ return {
705
+ success: false,
706
+ error: `Project ${projectSlug} not found. Deploy your project first.`,
707
+ }
708
+ }
709
+
710
+ const now = new Date().toISOString()
711
+ const verificationToken = generateVerificationToken()
712
+
713
+ const entry: CustomDomainEntry = {
714
+ customDomain,
715
+ projectSlug,
716
+ status: 'pending',
717
+ verificationToken,
718
+ createdAt: now,
719
+ }
720
+
721
+ // Add to both lookups
722
+ registry.customDomains[customDomain] = entry
723
+ registry.projectToCustomDomain[projectSlug] = customDomain
724
+
725
+ await saveRegistry(registry)
726
+
727
+ return { success: true, entry }
728
+ }
729
+
730
+ /**
731
+ * Update custom domain status
732
+ */
733
+ export async function updateCustomDomainStatus(
734
+ customDomain: string,
735
+ status: CustomDomainStatus,
736
+ additionalFields?: Partial<CustomDomainEntry>
737
+ ): Promise<boolean> {
738
+ const registry = await getRegistry()
739
+
740
+ if (!registry.customDomains?.[customDomain]) {
741
+ return false
742
+ }
743
+
744
+ registry.customDomains[customDomain] = {
745
+ ...registry.customDomains[customDomain],
746
+ status,
747
+ lastCheckedAt: new Date().toISOString(),
748
+ ...additionalFields,
749
+ }
750
+
751
+ // Set verifiedAt when transitioning to dns_verified
752
+ if (status === 'dns_verified' && !registry.customDomains[customDomain].verifiedAt) {
753
+ registry.customDomains[customDomain].verifiedAt = new Date().toISOString()
754
+ }
755
+
756
+ await saveRegistry(registry)
757
+ return true
758
+ }
759
+
760
+ /**
761
+ * Remove a custom domain from a project
762
+ */
763
+ export async function removeCustomDomain(
764
+ customDomain: string,
765
+ projectSlug: string
766
+ ): Promise<{ success: boolean; error?: string }> {
767
+ const registry = await getRegistry()
768
+
769
+ // Check if domain exists and belongs to this project
770
+ const entry = registry.customDomains?.[customDomain]
771
+ if (!entry) {
772
+ return { success: false, error: `Domain ${customDomain} not found.` }
773
+ }
774
+
775
+ if (entry.projectSlug !== projectSlug) {
776
+ return { success: false, error: `Domain ${customDomain} does not belong to this project.` }
777
+ }
778
+
779
+ // Remove from both lookups
780
+ delete registry.customDomains[customDomain]
781
+ delete registry.projectToCustomDomain[projectSlug]
782
+
783
+ await saveRegistry(registry)
784
+
785
+ return { success: true }
786
+ }
787
+
788
+ /**
789
+ * List all custom domains (for admin purposes)
790
+ */
791
+ export async function listCustomDomains(): Promise<CustomDomainEntry[]> {
792
+ const registry = await getRegistry()
793
+ return Object.values(registry.customDomains || {})
794
+ }
795
+
558
796
  /**
559
797
  * Generate a secure API key
560
798
  */
@@ -1 +0,0 @@
1
- <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -1 +0,0 @@
1
- <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>