@getdashi/cli 0.1.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.
Files changed (35) hide show
  1. package/LICENSE +631 -0
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +63 -0
  4. package/dist/commands/app-create.d.ts +6 -0
  5. package/dist/commands/app-create.js +89 -0
  6. package/dist/commands/app-dev.d.ts +6 -0
  7. package/dist/commands/app-dev.js +63 -0
  8. package/dist/entry.d.ts +2 -0
  9. package/dist/entry.js +13 -0
  10. package/dist/utils/serve-preview.d.ts +8 -0
  11. package/dist/utils/serve-preview.js +50 -0
  12. package/dist/utils/ui.d.ts +17 -0
  13. package/dist/utils/ui.js +56 -0
  14. package/dist/utils/wait-for-port.d.ts +6 -0
  15. package/dist/utils/wait-for-port.js +28 -0
  16. package/package.json +34 -0
  17. package/preview-dist/assets/index-77pJcSpc.css +1 -0
  18. package/preview-dist/assets/index-Bi2y3XyQ.js +11069 -0
  19. package/preview-dist/assets/index-TykmCjcq.js +56 -0
  20. package/preview-dist/index.html +18 -0
  21. package/templates/dashi-app/.prettierignore +4 -0
  22. package/templates/dashi-app/.prettierrc +9 -0
  23. package/templates/dashi-app/app/globals.css +29 -0
  24. package/templates/dashi-app/app/layout.tsx +22 -0
  25. package/templates/dashi-app/app/page.tsx +91 -0
  26. package/templates/dashi-app/app/preferences/page.tsx +95 -0
  27. package/templates/dashi-app/app/preferences.tsx +25 -0
  28. package/templates/dashi-app/app/providers.tsx +9 -0
  29. package/templates/dashi-app/eslint.config.js +23 -0
  30. package/templates/dashi-app/middleware.ts +46 -0
  31. package/templates/dashi-app/next.config.mjs +15 -0
  32. package/templates/dashi-app/package.json +36 -0
  33. package/templates/dashi-app/postcss.config.mjs +5 -0
  34. package/templates/dashi-app/public/manifest.json +10 -0
  35. package/templates/dashi-app/tsconfig.json +24 -0
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Dashi Preview</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; }
9
+ html, body, #root { height: 100%; margin: 0; padding: 0; background: #0e0e10; }
10
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color-scheme: dark; }
11
+ </style>
12
+ <script type="module" crossorigin src="/_preview/assets/index-TykmCjcq.js"></script>
13
+ <link rel="stylesheet" crossorigin href="/_preview/assets/index-77pJcSpc.css">
14
+ </head>
15
+ <body>
16
+ <div id="root"></div>
17
+ </body>
18
+ </html>
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ .next
3
+ dist
4
+ *.json
@@ -0,0 +1,9 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "trailingComma": "es5",
6
+ "printWidth": 100,
7
+ "bracketSpacing": true,
8
+ "arrowParens": "avoid"
9
+ }
@@ -0,0 +1,29 @@
1
+ @import "tailwindcss";
2
+
3
+ @keyframes rainbow {
4
+ 0% { color: hsl(0, 70%, 68%); }
5
+ 20% { color: hsl(60, 70%, 62%); }
6
+ 40% { color: hsl(140, 60%, 58%); }
7
+ 60% { color: hsl(210, 75%, 65%); }
8
+ 80% { color: hsl(280, 65%, 68%); }
9
+ 100% { color: hsl(360, 70%, 68%); }
10
+ }
11
+
12
+ @keyframes rainbowGlow {
13
+ 0% { filter: drop-shadow(0 0 18px hsla(0, 70%, 68%, var(--glow-alpha, 0))); }
14
+ 20% { filter: drop-shadow(0 0 18px hsla(60, 70%, 62%, var(--glow-alpha, 0))); }
15
+ 40% { filter: drop-shadow(0 0 18px hsla(140, 60%, 58%, var(--glow-alpha, 0))); }
16
+ 60% { filter: drop-shadow(0 0 18px hsla(210, 75%, 65%, var(--glow-alpha, 0))); }
17
+ 80% { filter: drop-shadow(0 0 18px hsla(280, 65%, 68%, var(--glow-alpha, 0))); }
18
+ 100% { filter: drop-shadow(0 0 18px hsla(360, 70%, 68%, var(--glow-alpha, 0))); }
19
+ }
20
+
21
+ @keyframes float {
22
+ 0%, 100% { transform: translateY(0); }
23
+ 50% { transform: translateY(-10px); }
24
+ }
25
+
26
+ @keyframes pulse-logo {
27
+ 0%, 100% { transform: scale(1); opacity: 0.8; }
28
+ 50% { transform: scale(1.05); opacity: 1; }
29
+ }
@@ -0,0 +1,22 @@
1
+ import type { Metadata, Viewport } from 'next'
2
+ import { DashiProvider } from './providers'
3
+ import './globals.css'
4
+
5
+ export const metadata: Metadata = {
6
+ title: '{{APP_NAME}}',
7
+ }
8
+
9
+ export const viewport: Viewport = {
10
+ width: 'device-width',
11
+ initialScale: 1,
12
+ }
13
+
14
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
15
+ return (
16
+ <html lang="en">
17
+ <body>
18
+ <DashiProvider>{children}</DashiProvider>
19
+ </body>
20
+ </html>
21
+ )
22
+ }
@@ -0,0 +1,91 @@
1
+ 'use client'
2
+
3
+ import { useDashiContext } from '@getdashi/client/react'
4
+ import { usePreferences } from './preferences'
5
+
6
+ // undefined = inherit currentColor from parent so the logo adapts to any background
7
+ const COLOR_VALUE: Record<string, string | undefined> = {
8
+ rainbow: undefined, // driven by keyframe animation
9
+ monochrome: undefined, // inherits parent text color
10
+ purple: 'rgba(167,139,250,1)',
11
+ }
12
+
13
+ const GLOW_VALUE: Record<string, string> = {
14
+ rainbow: '', // driven by rainbowGlow keyframe + --glow-alpha
15
+ monochrome: 'drop-shadow(0 0 18px rgba(255,255,255,0.2))',
16
+ purple: 'drop-shadow(0 0 18px rgba(167,139,250,0.35))',
17
+ }
18
+
19
+ export default function Page() {
20
+ const [{ color, animation, glow }] = usePreferences()
21
+ const { preferencesOpen, inputCaptured, openPreferences } = useDashiContext()
22
+
23
+ const logoAnimations: string[] = []
24
+ if (color === 'rainbow') logoAnimations.push('rainbow 4s linear infinite')
25
+ // Always run rainbowGlow when color=rainbow so it stays in sync; --glow-alpha controls opacity.
26
+ if (color === 'rainbow') logoAnimations.push('rainbowGlow 4s linear infinite')
27
+ if (animation === 'float') logoAnimations.push('float 3s ease-in-out infinite')
28
+ if (animation === 'pulse') logoAnimations.push('pulse-logo 2s ease-in-out infinite')
29
+
30
+ const filterValue = color !== 'rainbow' ? (glow ? GLOW_VALUE[color] : 'none') : undefined
31
+ const isDisabled = preferencesOpen || inputCaptured
32
+
33
+ return (
34
+ <main className="flex flex-col items-center justify-center min-h-screen bg-[#151617] text-white/90 font-sans">
35
+ <div
36
+ style={
37
+ {
38
+ width: 'min(140px, 28vw)',
39
+ color: COLOR_VALUE[color],
40
+ animation: logoAnimations.length ? logoAnimations.join(', ') : 'none',
41
+ filter: filterValue,
42
+ '--glow-alpha': color === 'rainbow' ? (glow ? '0.4' : '0') : undefined,
43
+ } as React.CSSProperties
44
+ }
45
+ className="mb-4 transition-[filter,color] duration-300 ease-[ease]"
46
+ >
47
+ <LogoIcon />
48
+ </div>
49
+
50
+ <h1 className="text-[clamp(26px,5vw,40px)] font-serif italic font-normal tracking-[-0.01em] mb-[10px] text-white/90">
51
+ Welcome to your app!
52
+ </h1>
53
+
54
+ <p className="text-[clamp(16px,3vw,22px)] text-white/45 tracking-[-0.01em] mb-5">
55
+ Edit <Code>app/page.tsx</Code> to get started.
56
+ </p>
57
+
58
+ <button
59
+ type="button"
60
+ onClick={openPreferences}
61
+ disabled={isDisabled}
62
+ className={`bg-white/3 border border-white/12 text-white/70 text-[13px] px-[18px] py-2 rounded-lg transition-[opacity,background] duration-200 ease-[ease] ${isDisabled ? 'opacity-30 cursor-default' : 'cursor-pointer'}`}
63
+ >
64
+ Preferences →
65
+ </button>
66
+ </main>
67
+ )
68
+ }
69
+
70
+ function Code({ children }: { children: React.ReactNode }) {
71
+ return (
72
+ <code className="font-mono text-[0.75em] bg-black/30 border border-white/10 rounded-[6px] py-[6px] px-[6px] text-white/45">
73
+ {children}
74
+ </code>
75
+ )
76
+ }
77
+
78
+ function LogoIcon() {
79
+ return (
80
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="44 98 414 304" fill="currentColor">
81
+ <path
82
+ d="M456.3,365.6L427,127.1c-1.9-15.1-14.7-26.5-30-26.5H88.8c-16,0-29.2,12.5-30.1,28.4L47.1,329.6c-0.6,9.7,3.1,19.2,9.9,26.1
83
+ l33,32.9c7,7,16.4,10.9,26.4,10.9h310c8.6,0,16.9-3.7,22.6-10.2C454.7,382.8,457.4,374.1,456.3,365.6z M113.4,211.1l-6.4-52.5
84
+ c-1.1-9.1,5.4-16.5,14.5-16.5h93.8c9.1,0,17.4,7.4,18.6,16.5l6.4,52.5c1.1,9.1-5.4,16.5-14.5,16.5h-93.8
85
+ C122.8,227.6,114.5,220.2,113.4,211.1z M239.6,340.4h-93.8c-9.1,0-17.4-7.4-18.6-16.5l-6.3-51.5c-1.1-9.1,5.4-16.5,14.5-16.5h93.8
86
+ c9.1,0,17.4,7.4,18.6,16.5l6.3,51.5C255.2,333,248.7,340.4,239.6,340.4z M394.6,340.4h-93.8c-9.1,0-17.4-7.4-18.6-16.5L262,158.6
87
+ c-1.1-9.1,5.4-16.5,14.5-16.5h93.8c9.1,0,17.4,7.4,18.6,16.5l20.3,165.3C410.2,333,403.7,340.4,394.6,340.4z"
88
+ />
89
+ </svg>
90
+ )
91
+ }
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ import { type Animation, type Color, usePreferences } from '../preferences'
4
+
5
+ const COLOR_OPTIONS: { value: Color; label: string }[] = [
6
+ { value: 'rainbow', label: 'Rainbow' },
7
+ { value: 'purple', label: 'Purple' },
8
+ { value: 'monochrome', label: 'Mono' },
9
+ ]
10
+
11
+ const ANIMATION_OPTIONS: { value: Animation; label: string }[] = [
12
+ { value: 'float', label: 'Float' },
13
+ { value: 'pulse', label: 'Pulse' },
14
+ { value: 'none', label: 'None' },
15
+ ]
16
+
17
+ export default function PreferencesPage() {
18
+ const [preferences, setPreferences] = usePreferences()
19
+
20
+ return (
21
+ <main className="flex flex-col min-h-screen font-mono bg-[#0e0e10] text-[#e5e5e5] p-5">
22
+ <div className="text-sky-500 text-[11px] tracking-[0.08em] uppercase font-semibold mb-4">
23
+ ◆ preferences
24
+ </div>
25
+
26
+ <div className="flex flex-col gap-[14px]">
27
+ <PrefRow label="Color">
28
+ {COLOR_OPTIONS.map(opt => (
29
+ <Chip
30
+ key={opt.value}
31
+ active={preferences.color === opt.value}
32
+ onClick={() => setPreferences({ color: opt.value })}
33
+ >
34
+ {opt.label}
35
+ </Chip>
36
+ ))}
37
+ </PrefRow>
38
+
39
+ <PrefRow label="Animation">
40
+ {ANIMATION_OPTIONS.map(opt => (
41
+ <Chip
42
+ key={opt.value}
43
+ active={preferences.animation === opt.value}
44
+ onClick={() => setPreferences({ animation: opt.value })}
45
+ >
46
+ {opt.label}
47
+ </Chip>
48
+ ))}
49
+ </PrefRow>
50
+
51
+ <PrefRow label="Glow">
52
+ <Chip active={preferences.glow} onClick={() => setPreferences({ glow: true })}>
53
+ On
54
+ </Chip>
55
+ <Chip active={!preferences.glow} onClick={() => setPreferences({ glow: false })}>
56
+ Off
57
+ </Chip>
58
+ </PrefRow>
59
+ </div>
60
+ </main>
61
+ )
62
+ }
63
+
64
+ function PrefRow({ label, children }: { label: string; children: React.ReactNode }) {
65
+ return (
66
+ <div className="flex items-center gap-3 text-xs">
67
+ <span className="text-gray-500 min-w-[80px] shrink-0">{label}</span>
68
+ <div className="flex gap-1 flex-wrap">{children}</div>
69
+ </div>
70
+ )
71
+ }
72
+
73
+ function Chip({
74
+ active,
75
+ onClick,
76
+ children,
77
+ }: {
78
+ active: boolean
79
+ onClick: () => void
80
+ children: React.ReactNode
81
+ }) {
82
+ return (
83
+ <button
84
+ type="button"
85
+ onClick={onClick}
86
+ className={`px-[10px] py-1 rounded border text-[11px] cursor-pointer font-[inherit] transition-[border-color,background,color] duration-150 ${
87
+ active
88
+ ? 'border-sky-500 bg-sky-500/10 text-sky-500'
89
+ : 'border-neutral-600 bg-transparent text-gray-500'
90
+ }`}
91
+ >
92
+ {children}
93
+ </button>
94
+ )
95
+ }
@@ -0,0 +1,25 @@
1
+ 'use client'
2
+
3
+ import { buildDashiProvider } from '@getdashi/client/react'
4
+
5
+ const COLORS = ['rainbow', 'purple', 'monochrome'] as const
6
+ const ANIMATIONS = ['float', 'pulse', 'none'] as const
7
+ export type Color = (typeof COLORS)[number]
8
+ export type Animation = (typeof ANIMATIONS)[number]
9
+
10
+ /**
11
+ * Type guard: narrows `unknown` to a member of a readonly tuple.
12
+ */
13
+ function isOneOf<T>(options: readonly T[], value: unknown): value is T {
14
+ return (options as readonly unknown[]).includes(value)
15
+ }
16
+
17
+ export const { Provider: DashiClientProvider, usePreferences } = buildDashiProvider({
18
+ preferences(stored) {
19
+ return {
20
+ color: isOneOf(COLORS, stored.color) ? stored.color : 'rainbow',
21
+ animation: isOneOf(ANIMATIONS, stored.animation) ? stored.animation : 'float',
22
+ glow: stored.glow === true,
23
+ }
24
+ },
25
+ })
@@ -0,0 +1,9 @@
1
+ // Next.js only exposes the request cookie header inside server components,
2
+ // but preference state lives in React context (client-only). createDashiProvider
3
+ // bridges the gap: it's a server component that reads the cookie and passes it
4
+ // to DashiClientProvider as a plain string — the only value that can cross the
5
+ // server → client boundary.
6
+ import { createDashiProvider } from '@getdashi/client/next'
7
+ import { DashiClientProvider } from './preferences'
8
+
9
+ export const DashiProvider = createDashiProvider({ clientProvider: DashiClientProvider })
@@ -0,0 +1,23 @@
1
+ import eslint from '@eslint/js'
2
+ import tseslint from 'typescript-eslint'
3
+ import prettierConfig from 'eslint-config-prettier'
4
+
5
+ export default tseslint.config(
6
+ { ignores: ['node_modules/**', '.next/**', 'dist/**'] },
7
+ eslint.configs.recommended,
8
+ ...tseslint.configs.recommended,
9
+ prettierConfig,
10
+ {
11
+ rules: {
12
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
13
+ '@typescript-eslint/consistent-type-imports': [
14
+ 'error',
15
+ {
16
+ prefer: 'type-imports',
17
+ fixStyle: 'separate-type-imports',
18
+ disallowTypeAnnotations: false,
19
+ },
20
+ ],
21
+ },
22
+ }
23
+ )
@@ -0,0 +1,46 @@
1
+ import { type NextRequest, NextResponse } from 'next/server'
2
+
3
+ const CONTEXT_PARAM = 'dashi_context'
4
+ const INPUT_CAPTURED_PARAM = 'dashi_input_captured'
5
+
6
+ /**
7
+ * Dashi middleware.
8
+ *
9
+ * Reads URL params injected by the container and converts them to
10
+ * cookies so Next.js server components can read them on the first
11
+ * request (SSR).
12
+ *
13
+ * Dashi_context — signed JWT identifying the dashboard; kept
14
+ * long-term. dashi_input_captured — SSR hint: '1' when the app starts
15
+ * in a captured-input state (e.g. tap-to-fullscreen split). Updated
16
+ * each load.
17
+ */
18
+ export function middleware(request: NextRequest) {
19
+ const token = request.nextUrl.searchParams.get(CONTEXT_PARAM)
20
+ const inputCaptured = request.nextUrl.searchParams.get(INPUT_CAPTURED_PARAM)
21
+
22
+ if (!token && inputCaptured === null) return NextResponse.next()
23
+
24
+ const existing = request.headers.get('cookie') ?? ''
25
+ const requestHeaders = new Headers(request.headers)
26
+
27
+ // Build an updated cookie string with the new values injected.
28
+ const additions: string[] = []
29
+ if (token) additions.push(`${CONTEXT_PARAM}=${encodeURIComponent(token)}`)
30
+ if (inputCaptured !== null)
31
+ additions.push(`${INPUT_CAPTURED_PARAM}=${encodeURIComponent(inputCaptured)}`)
32
+ // Additions go first so they take precedence over any stale cookies with the
33
+ // same name already in the browser's cookie jar.
34
+ requestHeaders.set('cookie', [...additions, existing].filter(Boolean).join('; '))
35
+
36
+ const response = NextResponse.next({ request: { headers: requestHeaders } })
37
+ if (token) response.cookies.set(CONTEXT_PARAM, token, { path: '/', sameSite: 'lax' })
38
+ // Session-scoped: always reflects the current load's captured state.
39
+ if (inputCaptured !== null)
40
+ response.cookies.set(INPUT_CAPTURED_PARAM, inputCaptured, { path: '/', sameSite: 'lax' })
41
+ return response
42
+ }
43
+
44
+ export const config = {
45
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
46
+ }
@@ -0,0 +1,15 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ // The preview tool runs on a different origin, so manifest.json must be
4
+ // cross-origin readable.
5
+ async headers() {
6
+ return [
7
+ {
8
+ source: '/manifest.json',
9
+ headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }],
10
+ },
11
+ ]
12
+ },
13
+ }
14
+
15
+ export default nextConfig
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "{{APP_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "dashi app dev --port {{APP_PORT}}",
7
+ "build:dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "eslint .",
11
+ "lint:fix": "eslint . --fix",
12
+ "format": "prettier --write .",
13
+ "format:check": "prettier --check ."
14
+ },
15
+ "dependencies": {
16
+ "@getdashi/client": "latest",
17
+ "next": "^14.2.10",
18
+ "react": "^18.3.1",
19
+ "react-dom": "^18.3.1"
20
+ },
21
+ "devDependencies": {
22
+ "@getdashi/cli": "latest",
23
+ "@eslint/js": "^9.23.0",
24
+ "@tailwindcss/postcss": "^4.2.1",
25
+ "@types/node": "^20.11.19",
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.0.0",
28
+ "eslint": "^9.23.0",
29
+ "eslint-config-prettier": "^10.1.1",
30
+ "postcss": "^8.5.6",
31
+ "prettier": "^3.5.3",
32
+ "tailwindcss": "^4.2.1",
33
+ "typescript": "^5.3.3",
34
+ "typescript-eslint": "^8.28.0"
35
+ }
36
+ }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "manifest-version": "1",
3
+ "name": "{{APP_NAME}}",
4
+ "basePath": "/",
5
+ "preferencesPath": "/preferences",
6
+ "tapToFullscreen": true,
7
+ "overlayPreferencesButton": false,
8
+ "overlayBackButton": true,
9
+ "overlayFullscreenButton": false
10
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "allowImportingTsExtensions": true,
17
+ "plugins": [{ "name": "next" }],
18
+ "paths": {
19
+ "@/*": ["./*"]
20
+ }
21
+ },
22
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
23
+ "exclude": ["node_modules"]
24
+ }