@dilipod/ui 0.4.27 → 0.4.29

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,48 @@
1
+ /**
2
+ * Email Template Helpers
3
+ *
4
+ * Shared branded email template and HTML building blocks.
5
+ * Used by both platform and admin apps via their local `lib/email.ts` re-exports.
6
+ */
7
+ /**
8
+ * Wrap email body content in the standard Dilipod branded template.
9
+ *
10
+ * Usage:
11
+ * emailTemplate({
12
+ * body: '<h2>Hello</h2><p>Content here</p>',
13
+ * })
14
+ *
15
+ * Or with a preheader (hidden preview text in email clients):
16
+ * emailTemplate({
17
+ * preheader: 'Quick summary shown in inbox',
18
+ * body: '...',
19
+ * })
20
+ */
21
+ export declare function emailTemplate({ body, preheader }: {
22
+ body: string;
23
+ preheader?: string;
24
+ }): string;
25
+ /**
26
+ * Render a CTA button for use inside emailTemplate body.
27
+ *
28
+ * buttonHtml({ text: 'Reset Password', href: 'https://...' })
29
+ * buttonHtml({ text: 'View Incident', href: '...', variant: 'danger' })
30
+ */
31
+ export declare function buttonHtml({ text, href, variant, }: {
32
+ text: string;
33
+ href: string;
34
+ variant?: 'primary' | 'danger';
35
+ }): string;
36
+ /**
37
+ * Render an info box (grey background) for key details.
38
+ *
39
+ * infoBoxHtml('<strong>Status:</strong> Deployed')
40
+ */
41
+ export declare function infoBoxHtml(innerHtml: string): string;
42
+ /**
43
+ * Render a warning/note box (yellow background).
44
+ *
45
+ * noteBoxHtml('This link expires in 1 hour.')
46
+ */
47
+ export declare function noteBoxHtml(text: string): string;
48
+ //# sourceMappingURL=email.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../src/lib/email.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA0E/F;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,IAAI,EACJ,OAAmB,GACpB,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;CAC/B,GAAG,MAAM,CAYT;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIrD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIhD"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Formatting Utilities
3
+ *
4
+ * Shared text, currency, duration, and date formatting used across apps.
5
+ */
6
+ /**
7
+ * Convert a cent value to euros (numeric).
8
+ * e.g. 2900 → 29
9
+ */
10
+ export declare function formatCentsToEuros(cents: number): string;
11
+ /**
12
+ * Format a euro value as a display string.
13
+ * e.g. 299 → "€299" or 299.5 with decimals=2 → "€299.50"
14
+ */
15
+ export declare function formatEuros(euros: number, decimals?: number): string;
16
+ /**
17
+ * Format milliseconds as a human-readable duration.
18
+ * e.g. 1500 → "1.5s"
19
+ */
20
+ export declare function formatDuration(ms: number): string;
21
+ /**
22
+ * Format a date into a compact relative-time string.
23
+ * Returns "—" for null/undefined, "5m" for < 1 hour, "2h" for < 48 hours,
24
+ * or a relative distance like "3 days" for older dates.
25
+ */
26
+ export declare function formatRelativeTime(date: Date | string | null): string;
27
+ //# sourceMappingURL=formatting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatting.d.ts","sourceRoot":"","sources":["../../src/lib/formatting.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAKpE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAUrE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dilipod/ui",
3
- "version": "0.4.27",
3
+ "version": "0.4.29",
4
4
  "description": "Dilipod Design System - Shared UI components and styles",
5
5
  "author": "Dilipod <hello@dilipod.com>",
6
6
  "license": "MIT",
@@ -81,6 +81,7 @@
81
81
  "@xyflow/react": "^12.10.0",
82
82
  "class-variance-authority": "^0.7.1",
83
83
  "clsx": "^2.1.1",
84
+ "date-fns": "^4.1.0",
84
85
  "tailwind-merge": "^3.3.0",
85
86
  "tailwindcss-animate": "^1.0.7"
86
87
  },
@@ -110,7 +111,7 @@
110
111
  "tailwindcss": "^4.1.8",
111
112
  "tsup": "^8.5.0",
112
113
  "typescript": "^5.8.3",
113
- "vite": "^7.3.1",
114
+ "vite": "^6.0.0",
114
115
  "vitest": "^1.2.0"
115
116
  }
116
117
  }
@@ -0,0 +1,106 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../lib/utils'
5
+
6
+ export interface ExpandableSectionProps {
7
+ /** Unique key for this section */
8
+ sectionKey: string
9
+ /** Section label text */
10
+ label: string
11
+ /** Optional icon rendered before the label */
12
+ icon?: React.ReactNode
13
+ /** Optional count badge after the label */
14
+ count?: number
15
+ /** Whether the section is currently expanded */
16
+ expanded: boolean
17
+ /** Toggle callback */
18
+ onToggle: (key: string) => void
19
+ /** Section content */
20
+ children: React.ReactNode
21
+ /** Additional class for the outer container */
22
+ className?: string
23
+ /** Additional class for the content area */
24
+ contentClassName?: string
25
+ /** Whether to render the section at all (default: true) */
26
+ show?: boolean
27
+ }
28
+
29
+ export function ExpandableSection({
30
+ sectionKey,
31
+ label,
32
+ icon,
33
+ count,
34
+ expanded,
35
+ onToggle,
36
+ children,
37
+ className,
38
+ contentClassName,
39
+ show = true,
40
+ }: ExpandableSectionProps) {
41
+ if (!show) return null
42
+
43
+ return (
44
+ <div className={className}>
45
+ <button
46
+ onClick={() => onToggle(sectionKey)}
47
+ className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors w-full text-left"
48
+ type="button"
49
+ >
50
+ <svg
51
+ width="12"
52
+ height="12"
53
+ viewBox="0 0 12 12"
54
+ fill="none"
55
+ className={cn(
56
+ 'shrink-0 transition-transform',
57
+ expanded ? 'rotate-0' : '-rotate-90'
58
+ )}
59
+ >
60
+ <path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
61
+ </svg>
62
+ {icon}
63
+ <span>{label}</span>
64
+ {count !== undefined && (
65
+ <span className="text-muted-foreground/60">({count})</span>
66
+ )}
67
+ </button>
68
+ {expanded && (
69
+ <div className={cn('mt-2 pl-4', contentClassName)}>
70
+ {children}
71
+ </div>
72
+ )}
73
+ </div>
74
+ )
75
+ }
76
+
77
+ /**
78
+ * Hook to manage expandable section state.
79
+ * Returns [expandedSections, toggleSection] tuple.
80
+ */
81
+ export function useExpandedSections(initialExpanded: string[] = []) {
82
+ const [expanded, setExpanded] = React.useState<Set<string>>(
83
+ () => new Set(initialExpanded)
84
+ )
85
+
86
+ const toggle = React.useCallback((key: string) => {
87
+ setExpanded(prev => {
88
+ const next = new Set(prev)
89
+ if (next.has(key)) {
90
+ next.delete(key)
91
+ } else {
92
+ next.add(key)
93
+ }
94
+ return next
95
+ })
96
+ }, [])
97
+
98
+ const isExpanded = React.useCallback(
99
+ (key: string) => expanded.has(key),
100
+ [expanded]
101
+ )
102
+
103
+ return { expanded, toggle, isExpanded } as const
104
+ }
105
+
106
+ ExpandableSection.displayName = 'ExpandableSection'
@@ -0,0 +1,23 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ export function useServiceWorker() {
6
+ useEffect(() => {
7
+ if ('serviceWorker' in navigator) {
8
+ navigator.serviceWorker
9
+ .register('/sw.js')
10
+ .then((registration) => {
11
+ console.log('Service Worker registered:', registration.scope)
12
+
13
+ // Check for updates periodically
14
+ setInterval(() => {
15
+ registration.update()
16
+ }, 60 * 60 * 1000) // Check every hour
17
+ })
18
+ .catch((error) => {
19
+ console.error('Service Worker registration failed:', error)
20
+ })
21
+ }
22
+ }, [])
23
+ }
package/src/index.ts CHANGED
@@ -305,6 +305,19 @@ export type { WorkerSpecProps, WorkerSpecDocumentation, AnalysisSource, Analysis
305
305
  export { FlowchartDiagram } from './components/flowchart-diagram'
306
306
  export type { FlowchartDiagramProps } from './components/flowchart-diagram'
307
307
 
308
+ // Expandable Section
309
+ export { ExpandableSection, useExpandedSections } from './components/expandable-section'
310
+ export type { ExpandableSectionProps } from './components/expandable-section'
311
+
312
+ // Hooks
313
+ export { useServiceWorker } from './hooks/use-service-worker'
314
+
315
+ // Formatting Utilities
316
+ export { formatCentsToEuros, formatEuros, formatDuration, formatRelativeTime } from './lib/formatting'
317
+
318
+ // Email Template Helpers
319
+ export { emailTemplate, buttonHtml, infoBoxHtml, noteBoxHtml } from './lib/email'
320
+
308
321
  // Utilities
309
322
  export { cn } from './lib/utils'
310
323
 
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Email Template Helpers
3
+ *
4
+ * Shared branded email template and HTML building blocks.
5
+ * Used by both platform and admin apps via their local `lib/email.ts` re-exports.
6
+ */
7
+
8
+ /**
9
+ * Wrap email body content in the standard Dilipod branded template.
10
+ *
11
+ * Usage:
12
+ * emailTemplate({
13
+ * body: '<h2>Hello</h2><p>Content here</p>',
14
+ * })
15
+ *
16
+ * Or with a preheader (hidden preview text in email clients):
17
+ * emailTemplate({
18
+ * preheader: 'Quick summary shown in inbox',
19
+ * body: '...',
20
+ * })
21
+ */
22
+ export function emailTemplate({ body, preheader }: { body: string; preheader?: string }): string {
23
+ const preheaderHtml = preheader
24
+ ? `<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;">${preheader}</div>`
25
+ : ''
26
+
27
+ return `<!DOCTYPE html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="utf-8" />
31
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
32
+ <meta name="color-scheme" content="light" />
33
+ <meta name="supported-color-schemes" content="light" />
34
+ <title>Dilipod</title>
35
+ </head>
36
+ <body style="margin: 0; padding: 0; background-color: #f0f0f0; -webkit-font-smoothing: antialiased;">
37
+ ${preheaderHtml}
38
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f0f0f0;">
39
+ <tr>
40
+ <td align="center" style="padding: 40px 16px;">
41
+ <!-- Main card -->
42
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width: 560px;">
43
+ <!-- Logo header -->
44
+ <tr>
45
+ <td style="padding: 0 0 24px 0;">
46
+ <table role="presentation" cellpadding="0" cellspacing="0">
47
+ <tr>
48
+ <td style="width: 32px; height: 32px; background-color: #00e5cc; border-radius: 6px; text-align: center; vertical-align: middle; font-size: 16px; font-weight: 700; color: #111111; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
49
+ D
50
+ </td>
51
+ <td style="padding-left: 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 17px; font-weight: 600; color: #111111; letter-spacing: -0.2px;">
52
+ Dilipod
53
+ </td>
54
+ </tr>
55
+ </table>
56
+ </td>
57
+ </tr>
58
+ <!-- Content card -->
59
+ <tr>
60
+ <td>
61
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 6px; overflow: hidden; border: 1px solid #e2e2e2;">
62
+ <!-- Accent bar -->
63
+ <tr>
64
+ <td style="height: 3px; background: linear-gradient(90deg, #00e5cc 0%, #00c8b5 100%); font-size: 0; line-height: 0;">&nbsp;</td>
65
+ </tr>
66
+ <!-- Body content -->
67
+ <tr>
68
+ <td style="padding: 36px 40px 40px 40px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; line-height: 1.7; color: #374151;">
69
+ ${body}
70
+ </td>
71
+ </tr>
72
+ </table>
73
+ </td>
74
+ </tr>
75
+ <!-- Footer -->
76
+ <tr>
77
+ <td style="padding: 24px 4px 0 4px;">
78
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0">
79
+ <tr>
80
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: #9ca3af;">
81
+ &copy; ${new Date().getFullYear()} Dilipod &mdash; Your Digital Workforce
82
+ </td>
83
+ <td align="right" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px;">
84
+ <a href="https://dilipod.com" style="color: #9ca3af; text-decoration: none;">dilipod.com</a>
85
+ </td>
86
+ </tr>
87
+ </table>
88
+ </td>
89
+ </tr>
90
+ </table>
91
+ </td>
92
+ </tr>
93
+ </table>
94
+ </body>
95
+ </html>`
96
+ }
97
+
98
+ /**
99
+ * Render a CTA button for use inside emailTemplate body.
100
+ *
101
+ * buttonHtml({ text: 'Reset Password', href: 'https://...' })
102
+ * buttonHtml({ text: 'View Incident', href: '...', variant: 'danger' })
103
+ */
104
+ export function buttonHtml({
105
+ text,
106
+ href,
107
+ variant = 'primary',
108
+ }: {
109
+ text: string
110
+ href: string
111
+ variant?: 'primary' | 'danger'
112
+ }): string {
113
+ const bg = variant === 'danger' ? '#dc2626' : '#111111'
114
+ const color = '#ffffff'
115
+ return `<table role="presentation" cellpadding="0" cellspacing="0" style="margin-top: 24px;">
116
+ <tr>
117
+ <td style="background-color: ${bg}; border-radius: 6px;">
118
+ <a href="${href}" style="display: inline-block; padding: 12px 32px; color: ${color}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 600; text-decoration: none;">
119
+ ${text} &rarr;
120
+ </a>
121
+ </td>
122
+ </tr>
123
+ </table>`
124
+ }
125
+
126
+ /**
127
+ * Render an info box (grey background) for key details.
128
+ *
129
+ * infoBoxHtml('<strong>Status:</strong> Deployed')
130
+ */
131
+ export function infoBoxHtml(innerHtml: string): string {
132
+ return `<div style="background-color: #f8fafb; padding: 16px 20px; border-radius: 6px; border: 1px solid #e5e7eb; margin: 20px 0; border-left: 3px solid #00e5cc;">
133
+ ${innerHtml}
134
+ </div>`
135
+ }
136
+
137
+ /**
138
+ * Render a warning/note box (yellow background).
139
+ *
140
+ * noteBoxHtml('This link expires in 1 hour.')
141
+ */
142
+ export function noteBoxHtml(text: string): string {
143
+ return `<div style="background-color: #fffbeb; padding: 14px 18px; border-radius: 6px; border: 1px solid #fde68a; margin: 20px 0; font-size: 13px; color: #92400e; border-left: 3px solid #f59e0b;">
144
+ ${text}
145
+ </div>`
146
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Formatting Utilities
3
+ *
4
+ * Shared text, currency, duration, and date formatting used across apps.
5
+ */
6
+
7
+ import { differenceInHours, differenceInMinutes, formatDistanceToNow } from 'date-fns'
8
+
9
+ /**
10
+ * Convert a cent value to euros (numeric).
11
+ * e.g. 2900 → 29
12
+ */
13
+ export function formatCentsToEuros(cents: number): string {
14
+ return `€${(cents / 100).toLocaleString()}`
15
+ }
16
+
17
+ /**
18
+ * Format a euro value as a display string.
19
+ * e.g. 299 → "€299" or 299.5 with decimals=2 → "€299.50"
20
+ */
21
+ export function formatEuros(euros: number, decimals?: number): string {
22
+ if (decimals !== undefined) {
23
+ return `€${euros.toFixed(decimals)}`
24
+ }
25
+ return `€${euros.toLocaleString()}`
26
+ }
27
+
28
+ /**
29
+ * Format milliseconds as a human-readable duration.
30
+ * e.g. 1500 → "1.5s"
31
+ */
32
+ export function formatDuration(ms: number): string {
33
+ return `${(ms / 1000).toFixed(1)}s`
34
+ }
35
+
36
+ /**
37
+ * Format a date into a compact relative-time string.
38
+ * Returns "—" for null/undefined, "5m" for < 1 hour, "2h" for < 48 hours,
39
+ * or a relative distance like "3 days" for older dates.
40
+ */
41
+ export function formatRelativeTime(date: Date | string | null): string {
42
+ if (!date) return '—'
43
+ const d = typeof date === 'string' ? new Date(date) : date
44
+ const hours = differenceInHours(new Date(), d)
45
+ if (hours < 1) {
46
+ const mins = differenceInMinutes(new Date(), d)
47
+ return `${mins}m`
48
+ }
49
+ if (hours < 48) return `${hours}h`
50
+ return formatDistanceToNow(d, { addSuffix: false })
51
+ }