@northlight/ui 2.39.4 → 2.41.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@northlight/ui",
3
- "version": "2.39.4",
3
+ "version": "2.41.0",
4
4
  "description": "Northlight UI library, based on Chakra-ui",
5
5
  "license": "MIT",
6
6
  "author": "Camphouse",
@@ -11,17 +11,23 @@
11
11
  "types": "./dist/es/northlight.d.ts",
12
12
  "import": "./dist/es/northlight.js",
13
13
  "require": "./dist/umd/northlight.cjs"
14
- }
14
+ },
15
+ "./sandbox": "./sandbox/lib/index.ts"
16
+ },
17
+ "bin": {
18
+ "sandbox": "./sandbox/bin/sandbox.sh"
15
19
  },
16
20
  "main": "./dist/es/northlight.js",
17
21
  "types": "./dist/es/northlight.d.ts",
18
22
  "files": [
19
- "dist"
23
+ "dist",
24
+ "sandbox"
20
25
  ],
21
26
  "scripts": {
22
27
  "clean": "rm -rf dist",
23
- "lint": "eslint --ext ts,tsx lib/ test/",
28
+ "lint": "eslint --ext ts,tsx lib/ test/ sandbox/ northlight-sandbox/",
24
29
  "prepublishOnly": "yarn clean && yarn transpile",
30
+ "sandbox": "./sandbox/bin/sandbox.sh northlight-sandbox/scenarios.tsx",
25
31
  "test": "yarn mtft test",
26
32
  "transpile": "rollup -c",
27
33
  "watch": "yarn transpile -w"
@@ -45,13 +51,13 @@
45
51
  "@emotion/styled": "^11.11.0",
46
52
  "@hookform/resolvers": "^3.3.2",
47
53
  "@internationalized/date": "^3.5.0",
48
- "@northlight/icons": "^1.6.5",
54
+ "@northlight/icons": "^1.7.0",
49
55
  "@northlight/tokens": "^1.4.7",
50
56
  "@react-aria/button": "^3.8.2",
51
57
  "@react-aria/calendar": "^3.5.0",
52
58
  "@react-aria/checkbox": "^3.11.0",
53
59
  "@react-aria/datepicker": "^3.7.0",
54
- "@react-aria/focus": "^3.14.1",
60
+ "@react-aria/focus": "3.14.1",
55
61
  "@react-aria/i18n": "^3.8.2",
56
62
  "@react-aria/radio": "^3.8.0",
57
63
  "@react-aria/utils": "^3.20.0",
@@ -65,6 +71,7 @@
65
71
  "@types/react-router-dom": "^5.3.2",
66
72
  "@types/react-virtualized-auto-sizer": "^1.0.1",
67
73
  "@types/react-window": "^1.8.5",
74
+ "@vitejs/plugin-react": "^4.3.0",
68
75
  "chakra-react-select": "4.7.2",
69
76
  "framer-motion": "^4",
70
77
  "joi": "^17.11.0",
@@ -78,6 +85,8 @@
78
85
  "react-virtualized-auto-sizer": "^1.0.20",
79
86
  "react-window": "^1.8.9",
80
87
  "sinon": "^15.2.0",
88
+ "tsx": "^4.21.0",
89
+ "vite": "^6.0.0",
81
90
  "yafu": "^2.1.4"
82
91
  },
83
92
  "devDependencies": {
@@ -101,12 +110,13 @@
101
110
  "rollup-plugin-dts": "^5.2.0",
102
111
  "rollup-plugin-esbuild": "^5.0.0",
103
112
  "sinon": "^15.2.0",
104
- "typescript": "^5.2.2"
113
+ "typescript": "^5.2.2",
114
+ "vitest": "^2.0.0"
105
115
  },
106
116
  "peerDependencies": {
107
117
  "react": ">=18.0.0",
108
118
  "react-dom": ">=18.0.0",
109
119
  "react-router-dom": "^5.0.0"
110
120
  },
111
- "gitHead": "0cfe849f4f94ed9be9fd1859d35b42b3589728bd"
121
+ "gitHead": "fd559db249b74856dcf44ba2615913607647637a"
112
122
  }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ DIR="$(dirname "$0")"
3
+ exec pnpx tsx "$DIR/sandbox.ts" "$@"
@@ -0,0 +1,133 @@
1
+ /* eslint-disable no-restricted-syntax */
2
+ import { existsSync } from 'node:fs'
3
+ import { dirname, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import react from '@vitejs/plugin-react'
6
+ import { createServer } from 'vite'
7
+
8
+ const DEFAULT_SCENARIOS_PATHS = [ 'sandbox/scenarios.ts', 'sandbox/scenarios.tsx' ]
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url))
11
+
12
+ function findScenariosFile (cwd: string, scenariosArg?: string): string | undefined {
13
+ if (scenariosArg) {
14
+ const absolutePath = resolve(cwd, scenariosArg)
15
+ return existsSync(absolutePath) ? absolutePath : undefined
16
+ }
17
+ for (const path of DEFAULT_SCENARIOS_PATHS) {
18
+ const absolutePath = resolve(cwd, path)
19
+ if (existsSync(absolutePath)) return absolutePath
20
+ }
21
+ return undefined
22
+ }
23
+
24
+ async function main () {
25
+ const cwd = process.cwd()
26
+ const scenariosArg = process.argv[2]
27
+ const absoluteScenariosPath = findScenariosFile(cwd, scenariosArg)
28
+
29
+ if (!absoluteScenariosPath) {
30
+ console.error('Scenarios file not found.')
31
+ console.error(
32
+ scenariosArg
33
+ ? `Please check the path: ${resolve(cwd, scenariosArg)}`
34
+ : `Create one of: ${DEFAULT_SCENARIOS_PATHS.join(', ')}`
35
+ )
36
+ process.exit(1)
37
+ }
38
+
39
+ const sandboxPackageDir = resolve(__dirname, '..')
40
+
41
+ const virtualEntryId = 'virtual:sandbox-entry.tsx'
42
+ const resolvedVirtualEntryId = `\0${virtualEntryId}`
43
+
44
+ const virtualHtmlId = 'virtual:sandbox-html'
45
+
46
+ const server = await createServer({
47
+ configFile: false,
48
+ root: cwd,
49
+ server: {
50
+ port: 5000,
51
+ strictPort: false,
52
+ },
53
+ plugins: [
54
+ react(),
55
+ {
56
+ name: 'sandbox-virtual-entry',
57
+ resolveId (id) {
58
+ if (id === virtualEntryId) return resolvedVirtualEntryId
59
+ if (id === '/' || id === '/index.html') return virtualHtmlId
60
+ return undefined
61
+ },
62
+ load (id) {
63
+ if (id === resolvedVirtualEntryId) {
64
+ return `
65
+ import { createElement } from 'react'
66
+ import { createRoot } from 'react-dom/client'
67
+ import { SandboxViewer } from '${sandboxPackageDir}/lib/viewer/sandbox-viewer.tsx'
68
+ import { scenarios } from '${absoluteScenariosPath}'
69
+
70
+ const root = document.getElementById('root')
71
+ createRoot(root).render(createElement(SandboxViewer, { scenarios }))
72
+ `
73
+ }
74
+ if (id === virtualHtmlId) {
75
+ return `
76
+ <!DOCTYPE html>
77
+ <html>
78
+ <head>
79
+ <meta charset="UTF-8" />
80
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
81
+ <title>Sandbox</title>
82
+ </head>
83
+ <body>
84
+ <div id="root"></div>
85
+ <script type="module" src="/@id/__x00__${virtualEntryId}"></script>
86
+ </body>
87
+ </html>
88
+ `
89
+ }
90
+ return undefined
91
+ },
92
+ },
93
+ {
94
+ name: 'sandbox-html-middleware',
95
+ configureServer (devServer) {
96
+ devServer.middlewares.use((req, res, next) => {
97
+ const url = req.url ?? ''
98
+ const isAsset =
99
+ url.startsWith('/@') || url.startsWith('/node_modules') || url.includes('.')
100
+
101
+ if (!isAsset) {
102
+ const html = `
103
+ <!DOCTYPE html>
104
+ <html>
105
+ <head>
106
+ <meta charset="UTF-8" />
107
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
108
+ <title>Sandbox</title>
109
+ </head>
110
+ <body>
111
+ <div id="root"></div>
112
+ <script type="module" src="/@id/__x00__${virtualEntryId}"></script>
113
+ </body>
114
+ </html>
115
+ `
116
+ devServer.transformIndexHtml(url, html).then((transformed) => {
117
+ res.setHeader('Content-Type', 'text/html')
118
+ res.end(transformed)
119
+ })
120
+ return
121
+ }
122
+ next()
123
+ })
124
+ },
125
+ },
126
+ ],
127
+ })
128
+
129
+ await server.listen()
130
+ server.printUrls()
131
+ }
132
+
133
+ main()
@@ -0,0 +1,3 @@
1
+ export { runScenarios } from './run-scenarios'
2
+ export type { ComponentScenarios, PlayContext, Scenario } from './types.ts'
3
+ export { SandboxViewer } from './viewer/sandbox-viewer'
@@ -0,0 +1,60 @@
1
+ /* eslint-disable no-restricted-syntax */
2
+ import { cleanup, render, screen, within } from '@testing-library/react'
3
+ import userEvent from '@testing-library/user-event'
4
+ import { createElement } from 'react'
5
+ import { MediatoolThemeProvider, theme } from '../../lib'
6
+ import type { ComponentScenarios, PlayContext } from './types.ts'
7
+
8
+ declare const describe: (name: string, fn: () => void) => void
9
+ declare const it: (name: string, fn: () => void | Promise<void>) => void
10
+
11
+ if (typeof window !== 'undefined' && !window.matchMedia) {
12
+ window.matchMedia = (query: string) => ({
13
+ matches: false,
14
+ media: query,
15
+ onchange: null,
16
+ addListener: () => {},
17
+ removeListener: () => {},
18
+ addEventListener: () => {},
19
+ removeEventListener: () => {},
20
+ dispatchEvent: () => false,
21
+ })
22
+ }
23
+
24
+ function runScenarios (allScenarios: ComponentScenarios[]): void {
25
+ for (const componentScenarios of allScenarios) {
26
+ const { name: componentName, component: defaultComponent, scenarios } = componentScenarios
27
+
28
+ describe(componentName, () => {
29
+ for (const scenario of scenarios) {
30
+ const { name: scenarioName, props, component: scenarioComponent, play } = scenario
31
+ const component = scenarioComponent ?? defaultComponent
32
+
33
+ it(scenarioName, async () => {
34
+ const user = userEvent.setup()
35
+ const { container } = render(
36
+ createElement(
37
+ MediatoolThemeProvider,
38
+ { theme },
39
+ createElement(component, props as Record<string, unknown>)
40
+ )
41
+ )
42
+
43
+ if (play) {
44
+ const context: PlayContext = {
45
+ screen: within(container),
46
+ documentScreen: screen,
47
+ user,
48
+ container,
49
+ }
50
+ await play(context)
51
+ }
52
+
53
+ cleanup()
54
+ })
55
+ }
56
+ })
57
+ }
58
+ }
59
+
60
+ export { runScenarios }
@@ -0,0 +1,35 @@
1
+ import type { screen, within } from '@testing-library/react'
2
+ import type userEvent from '@testing-library/user-event'
3
+ import type { ComponentType } from 'react'
4
+
5
+ type PlayContext = {
6
+ /** Queries scoped to the scenario container - use for non-portaled content */
7
+ screen: ReturnType<typeof within>
8
+ /** Queries scoped to document.body - use for portaled content (dropdowns, modals) */
9
+ documentScreen: typeof screen
10
+ user: ReturnType<typeof userEvent.setup>
11
+ container: HTMLElement
12
+ }
13
+
14
+ type Scenario<P> =
15
+ | {
16
+ name: string
17
+ props: P
18
+ component?: undefined
19
+ play?: (context: PlayContext) => Promise<void>
20
+ }
21
+ | {
22
+ name: string
23
+ props: Record<string, unknown>
24
+ component: ComponentType<any>
25
+ play?: (context: PlayContext) => Promise<void>
26
+ }
27
+
28
+ type ComponentScenarios<P = any> = {
29
+ name: string
30
+ component: ComponentType<P>
31
+ scenarios: Scenario<P>[]
32
+ inline?: boolean
33
+ }
34
+
35
+ export type { PlayContext, Scenario, ComponentScenarios }
@@ -0,0 +1,34 @@
1
+ import { Component, type ComponentType, type ReactNode, createElement } from 'react'
2
+
3
+ type ErrorBoundaryProps = {
4
+ children: ReactNode
5
+ fallback: ComponentType<{ error: Error }>
6
+ }
7
+
8
+ type ErrorBoundaryState = {
9
+ error: Error | null
10
+ }
11
+
12
+ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
13
+ constructor (props: ErrorBoundaryProps) {
14
+ super(props)
15
+ this.state = { error: null }
16
+ }
17
+
18
+ static getDerivedStateFromError (error: Error): ErrorBoundaryState {
19
+ return { error }
20
+ }
21
+
22
+ render () {
23
+ const { error } = this.state
24
+ const { children, fallback } = this.props
25
+
26
+ if (error) {
27
+ return createElement(fallback, { error })
28
+ }
29
+
30
+ return children
31
+ }
32
+ }
33
+
34
+ export { ErrorBoundary }
@@ -0,0 +1,328 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ body {
6
+ margin: 0;
7
+ background: #f8fafc;
8
+ }
9
+
10
+ .sandbox-viewer {
11
+ display: flex;
12
+ height: 100vh;
13
+ font-family:
14
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
15
+ color: #1e293b;
16
+ }
17
+
18
+ .sandbox-sidebar {
19
+ width: 220px;
20
+ background: #ffffff;
21
+ border-right: 1px solid #e2e8f0;
22
+ padding: 20px 0;
23
+ overflow-y: auto;
24
+ flex-shrink: 0;
25
+ }
26
+
27
+ .sandbox-search {
28
+ padding: 0 12px 16px;
29
+ }
30
+
31
+ .sandbox-search-input {
32
+ width: 100%;
33
+ padding: 8px 12px;
34
+ font-size: 13px;
35
+ border: 1px solid #e2e8f0;
36
+ border-radius: 6px;
37
+ background: #f8fafc;
38
+ color: #1e293b;
39
+ outline: none;
40
+ transition: all 0.15s ease;
41
+ }
42
+
43
+ .sandbox-search-input::placeholder {
44
+ color: #94a3b8;
45
+ }
46
+
47
+ .sandbox-search-input:focus {
48
+ border-color: #7c3aed;
49
+ background: #ffffff;
50
+ box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
51
+ }
52
+
53
+ .sandbox-title {
54
+ font-size: 11px;
55
+ font-weight: 600;
56
+ text-transform: uppercase;
57
+ color: #94a3b8;
58
+ padding: 0 20px 16px;
59
+ letter-spacing: 0.08em;
60
+ }
61
+
62
+ .sandbox-component {
63
+ display: block;
64
+ width: 100%;
65
+ padding: 10px 20px;
66
+ background: none;
67
+ border: none;
68
+ text-align: left;
69
+ cursor: pointer;
70
+ font-size: 14px;
71
+ color: #475569;
72
+ transition: all 0.15s ease;
73
+ }
74
+
75
+ .sandbox-component:hover {
76
+ background: #f1f5f9;
77
+ color: #1e293b;
78
+ }
79
+
80
+ .sandbox-component.selected {
81
+ background: linear-gradient(to right, #ede9fe, #f1f5f9);
82
+ color: #6d28d9;
83
+ font-weight: 500;
84
+ border-left: 3px solid #7c3aed;
85
+ padding-left: 17px;
86
+ }
87
+
88
+ .sandbox-main {
89
+ flex: 1;
90
+ display: flex;
91
+ flex-direction: column;
92
+ overflow: hidden;
93
+ background: #f8fafc;
94
+ }
95
+
96
+ .sandbox-tabs {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 4px;
100
+ padding: 0 24px;
101
+ background: #ffffff;
102
+ border-bottom: 1px solid #e2e8f0;
103
+ min-height: 52px;
104
+ overflow-x: auto;
105
+ flex-wrap: wrap;
106
+ }
107
+
108
+ .sandbox-tab {
109
+ padding: 14px 16px;
110
+ min-width: 100px;
111
+ text-align: center;
112
+ background: none;
113
+ border: none;
114
+ border-bottom: 2px solid transparent;
115
+ cursor: pointer;
116
+ font-size: 13px;
117
+ font-weight: 500;
118
+ color: #64748b;
119
+ margin-bottom: -1px;
120
+ transition: all 0.15s ease;
121
+ border-radius: 6px 6px 0 0;
122
+ }
123
+
124
+ .sandbox-tab:hover {
125
+ color: #475569;
126
+ background: #f8fafc;
127
+ }
128
+
129
+ .sandbox-tab.selected {
130
+ color: #7c3aed;
131
+ border-bottom-color: #7c3aed;
132
+ background: none;
133
+ }
134
+
135
+ .sandbox-play {
136
+ margin-left: auto;
137
+ padding: 8px 16px;
138
+ background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
139
+ color: white;
140
+ border: none;
141
+ border-radius: 6px;
142
+ cursor: pointer;
143
+ font-size: 13px;
144
+ font-weight: 500;
145
+ transition: all 0.15s ease;
146
+ box-shadow: 0 1px 2px rgba(124, 58, 237, 0.2);
147
+ }
148
+
149
+ .sandbox-play:hover {
150
+ background: linear-gradient(135deg, #6d28d9 0%, #5b21b6 100%);
151
+ box-shadow: 0 2px 4px rgba(124, 58, 237, 0.3);
152
+ transform: translateY(-1px);
153
+ }
154
+
155
+ .sandbox-play:disabled {
156
+ background: #cbd5e1;
157
+ cursor: not-allowed;
158
+ box-shadow: none;
159
+ transform: none;
160
+ }
161
+
162
+ .sandbox-preview {
163
+ flex: 1;
164
+ padding: 32px;
165
+ overflow: auto;
166
+ }
167
+
168
+ .sandbox-error {
169
+ background: #fef2f2;
170
+ border: 1px solid #fecaca;
171
+ border-radius: 8px;
172
+ padding: 20px;
173
+ font-family: system-ui, -apple-system, sans-serif;
174
+ }
175
+
176
+ .sandbox-error-title {
177
+ color: #dc2626;
178
+ font-weight: 600;
179
+ font-size: 14px;
180
+ margin-bottom: 12px;
181
+ }
182
+
183
+ .sandbox-error-message {
184
+ color: #b91c1c;
185
+ font-family: ui-monospace, monospace;
186
+ font-size: 13px;
187
+ margin: 0 0 12px;
188
+ padding: 12px;
189
+ background: #fff;
190
+ border-radius: 4px;
191
+ border: 1px solid #fecaca;
192
+ white-space: pre-wrap;
193
+ word-break: break-word;
194
+ }
195
+
196
+ .sandbox-error-stack {
197
+ color: #6b7280;
198
+ font-family: ui-monospace, monospace;
199
+ font-size: 11px;
200
+ margin: 0;
201
+ padding: 12px;
202
+ background: #fff;
203
+ border-radius: 4px;
204
+ border: 1px solid #e5e7eb;
205
+ white-space: pre-wrap;
206
+ word-break: break-word;
207
+ max-height: 300px;
208
+ overflow: auto;
209
+ }
210
+
211
+ .sandbox-inline {
212
+ flex: 1;
213
+ padding: 24px;
214
+ overflow: auto;
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: 16px;
218
+ }
219
+
220
+ .sandbox-inline-scenario {
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 16px;
224
+ }
225
+
226
+ .sandbox-inline-label {
227
+ font-size: 12px;
228
+ font-weight: 500;
229
+ color: #64748b;
230
+ text-transform: uppercase;
231
+ letter-spacing: 0.05em;
232
+ width: 100px;
233
+ flex-shrink: 0;
234
+ }
235
+
236
+ .sandbox-inline-preview {
237
+ flex: 1;
238
+ background: #ffffff;
239
+ border-radius: 8px;
240
+ padding: 16px;
241
+ border: 1px solid #e2e8f0;
242
+ }
243
+
244
+ .sandbox-play-slot {
245
+ width: 32px;
246
+ flex-shrink: 0;
247
+ }
248
+
249
+ .sandbox-play-inline {
250
+ width: 32px;
251
+ height: 32px;
252
+ padding: 0;
253
+ background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
254
+ color: white;
255
+ border: none;
256
+ border-radius: 6px;
257
+ cursor: pointer;
258
+ font-size: 12px;
259
+ transition: all 0.15s ease;
260
+ }
261
+
262
+ .sandbox-play-inline:hover {
263
+ background: linear-gradient(135deg, #6d28d9 0%, #5b21b6 100%);
264
+ transform: translateY(-1px);
265
+ }
266
+
267
+ .sandbox-play-inline:disabled {
268
+ background: #cbd5e1;
269
+ cursor: not-allowed;
270
+ transform: none;
271
+ }
272
+
273
+ .sandbox-play-inline.error {
274
+ background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
275
+ }
276
+
277
+ .sandbox-play.error {
278
+ background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
279
+ }
280
+
281
+ .sandbox-play.error:hover {
282
+ background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
283
+ }
284
+
285
+ .sandbox-play-error {
286
+ margin-top: 16px;
287
+ background: #fef2f2;
288
+ border: 1px solid #fecaca;
289
+ border-radius: 8px;
290
+ padding: 16px;
291
+ }
292
+
293
+ .sandbox-play-error-title {
294
+ color: #dc2626;
295
+ font-weight: 600;
296
+ font-size: 13px;
297
+ margin-bottom: 8px;
298
+ display: flex;
299
+ align-items: center;
300
+ gap: 6px;
301
+ }
302
+
303
+ .sandbox-play-error-title::before {
304
+ content: "✕";
305
+ display: inline-flex;
306
+ align-items: center;
307
+ justify-content: center;
308
+ width: 18px;
309
+ height: 18px;
310
+ background: #dc2626;
311
+ color: white;
312
+ border-radius: 50%;
313
+ font-size: 10px;
314
+ font-weight: bold;
315
+ }
316
+
317
+ .sandbox-play-error-message {
318
+ color: #b91c1c;
319
+ font-family: ui-monospace, monospace;
320
+ font-size: 12px;
321
+ margin: 0;
322
+ padding: 10px;
323
+ background: #fff;
324
+ border-radius: 4px;
325
+ border: 1px solid #fecaca;
326
+ white-space: pre-wrap;
327
+ word-break: break-word;
328
+ }