@bespokeagentics/create-microdots-host 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 (60) hide show
  1. package/LICENSE +202 -0
  2. package/package.json +26 -0
  3. package/src/cli.ts +50 -0
  4. package/src/index.ts +215 -0
  5. package/templates/base/README.md +49 -0
  6. package/templates/base/_gitignore +7 -0
  7. package/templates/base/host/index.html +33 -0
  8. package/templates/base/host/package.json +10 -0
  9. package/templates/base/host/public/themes/neutral-dark.css +109 -0
  10. package/templates/base/host/public/themes/neutral.css +109 -0
  11. package/templates/base/host/src/main.ts +202 -0
  12. package/templates/base/host/src/topology.test.ts +42 -0
  13. package/templates/base/host/topology.json +84 -0
  14. package/templates/base/host/vite.base.ts +41 -0
  15. package/templates/base/microdots/pulse/package.json +20 -0
  16. package/templates/base/microdots/pulse/service/handlers.ts +36 -0
  17. package/templates/base/microdots/pulse/service/main.ts +30 -0
  18. package/templates/base/microdots/pulse/src/app.ts +275 -0
  19. package/templates/base/microdots/pulse/src/client.ts +14 -0
  20. package/templates/base/microdots/pulse/src/contract.ts +32 -0
  21. package/templates/base/microdots/pulse/src/element.ts +24 -0
  22. package/templates/base/microdots/pulse/src/entry.ts +1 -0
  23. package/templates/base/microdots/pulse/src/surface.ts +30 -0
  24. package/templates/base/microdots/pulse/src/tokens.css +30 -0
  25. package/templates/base/microdots/scope-picker/package.json +20 -0
  26. package/templates/base/microdots/scope-picker/service/handlers.ts +15 -0
  27. package/templates/base/microdots/scope-picker/service/main.ts +30 -0
  28. package/templates/base/microdots/scope-picker/src/app.ts +256 -0
  29. package/templates/base/microdots/scope-picker/src/client.ts +18 -0
  30. package/templates/base/microdots/scope-picker/src/contract.ts +27 -0
  31. package/templates/base/microdots/scope-picker/src/element.ts +21 -0
  32. package/templates/base/microdots/scope-picker/src/entry.ts +1 -0
  33. package/templates/base/microdots/scope-picker/src/surface.ts +23 -0
  34. package/templates/base/microdots/scope-picker/src/tokens.css +30 -0
  35. package/templates/base/scripts/build-output.test.ts +71 -0
  36. package/templates/base/scripts/build-output.ts +28 -0
  37. package/templates/base/scripts/build.ts +37 -0
  38. package/templates/base/scripts/check-tokens.ts +63 -0
  39. package/templates/base/scripts/dev.ts +78 -0
  40. package/templates/base/scripts/discover.test.ts +133 -0
  41. package/templates/base/scripts/discover.ts +137 -0
  42. package/templates/base/scripts/dot-vite.ts +28 -0
  43. package/templates/base/scripts/emit-manifests.ts +154 -0
  44. package/templates/base/tsconfig.json +18 -0
  45. package/templates/base/types.d.ts +9 -0
  46. package/templates/base/vitest.config.ts +8 -0
  47. package/templates/plain/host/src/styles.css +46 -0
  48. package/templates/plain/host/vite.config.template.ts +4 -0
  49. package/templates/plain/microdots/pulse/src/styles.css +17 -0
  50. package/templates/plain/microdots/pulse/vite.config.template.ts +4 -0
  51. package/templates/plain/microdots/scope-picker/src/styles.css +19 -0
  52. package/templates/plain/microdots/scope-picker/vite.config.template.ts +4 -0
  53. package/templates/plain/package.json +39 -0
  54. package/templates/tailwind/host/src/styles.css +28 -0
  55. package/templates/tailwind/host/vite.config.template.ts +5 -0
  56. package/templates/tailwind/microdots/pulse/src/styles.css +24 -0
  57. package/templates/tailwind/microdots/pulse/vite.config.template.ts +5 -0
  58. package/templates/tailwind/microdots/scope-picker/src/styles.css +25 -0
  59. package/templates/tailwind/microdots/scope-picker/vite.config.template.ts +5 -0
  60. package/templates/tailwind/package.json +41 -0
@@ -0,0 +1,133 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { resolve } from 'node:path'
4
+ import { afterEach, describe, expect, test } from 'vitest'
5
+
6
+ import {
7
+ assertSurfaceTagsMatch,
8
+ listMicroDots,
9
+ registryFor,
10
+ } from './discover.ts'
11
+
12
+ const roots: string[] = []
13
+
14
+ const root = (): string => {
15
+ const directory = mkdtempSync(resolve(tmpdir(), 'microdots-discovery-'))
16
+ roots.push(directory)
17
+ return directory
18
+ }
19
+
20
+ const writeDot = (
21
+ directory: string,
22
+ name: string,
23
+ metadata: Readonly<Record<string, unknown>>,
24
+ ): void => {
25
+ const dot = resolve(directory, name)
26
+ mkdirSync(dot, { recursive: true })
27
+ writeFileSync(
28
+ resolve(dot, 'package.json'),
29
+ JSON.stringify({ name: `@starter/${name}`, microdot: metadata }),
30
+ )
31
+ }
32
+
33
+ afterEach(() => {
34
+ roots.splice(0).forEach(directory => {
35
+ rmSync(directory, { recursive: true, force: true })
36
+ })
37
+ })
38
+
39
+ describe('MicroDot discovery', () => {
40
+ test('one metadata record drives one registry row per tag', () => {
41
+ const directory = root()
42
+ writeDot(directory, 'one', {
43
+ tags: ['one-view', 'one-admin'],
44
+ port: 3991,
45
+ bundle: 'one.js',
46
+ })
47
+
48
+ expect(registryFor(listMicroDots(directory))).toEqual([
49
+ {
50
+ tag: 'one-view',
51
+ bundle: '/microdots/one.js',
52
+ localPort: 3991,
53
+ attributes: {},
54
+ },
55
+ {
56
+ tag: 'one-admin',
57
+ bundle: '/microdots/one.js',
58
+ localPort: 3991,
59
+ attributes: {},
60
+ },
61
+ ])
62
+ })
63
+
64
+ test('duplicate tags fail before orchestration', () => {
65
+ const directory = root()
66
+ writeDot(directory, 'one', {
67
+ tags: ['one-view'],
68
+ port: 3991,
69
+ bundle: 'same.js',
70
+ })
71
+ writeDot(directory, 'two', {
72
+ tags: ['one-view'],
73
+ port: 3992,
74
+ bundle: 'two.js',
75
+ })
76
+ expect(() => listMicroDots(directory)).toThrow('duplicate tag')
77
+ })
78
+
79
+ test('duplicate ports fail before orchestration', () => {
80
+ const directory = root()
81
+ writeDot(directory, 'one', {
82
+ tags: ['one-view'],
83
+ port: 3991,
84
+ bundle: 'one.js',
85
+ })
86
+ writeDot(directory, 'two', {
87
+ tags: ['two-view'],
88
+ port: 3991,
89
+ bundle: 'two.js',
90
+ })
91
+ expect(() => listMicroDots(directory)).toThrow('duplicate port')
92
+ })
93
+
94
+ test('duplicate bundles fail before orchestration', () => {
95
+ const directory = root()
96
+ writeDot(directory, 'one', {
97
+ tags: ['one-view'],
98
+ port: 3991,
99
+ bundle: 'same.js',
100
+ })
101
+ writeDot(directory, 'two', {
102
+ tags: ['two-view'],
103
+ port: 3992,
104
+ bundle: 'same.js',
105
+ })
106
+ expect(() => listMicroDots(directory)).toThrow('duplicate bundle')
107
+ })
108
+
109
+ test('surface tags must match package metadata exactly', () => {
110
+ const directory = root()
111
+ writeDot(directory, 'one', {
112
+ tags: ['one-view', 'one-admin'],
113
+ port: 3991,
114
+ bundle: 'one.js',
115
+ })
116
+ const dot = listMicroDots(directory)[0]
117
+ if (dot === undefined) throw new Error('fixture dot was not discovered')
118
+
119
+ expect(() =>
120
+ assertSurfaceTagsMatch(dot, [{ tag: 'one-admin' }, { tag: 'one-view' }]),
121
+ ).not.toThrow()
122
+ expect(() => assertSurfaceTagsMatch(dot, [{ tag: 'one-view' }])).toThrow(
123
+ 'do not match package.json microdot.tags',
124
+ )
125
+ expect(() =>
126
+ assertSurfaceTagsMatch(dot, [
127
+ { tag: 'one-view' },
128
+ { tag: 'one-admin' },
129
+ { tag: 'one-extra' },
130
+ ]),
131
+ ).toThrow('do not match package.json microdot.tags')
132
+ })
133
+ })
@@ -0,0 +1,137 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
+ import { basename, resolve } from 'node:path'
3
+ import type { Plugin } from 'vite'
4
+
5
+ export const repoRoot = resolve(import.meta.dirname, '..')
6
+ export const microdotsRoot = resolve(repoRoot, 'microdots')
7
+
8
+ export type MicroDotPackage = {
9
+ readonly name: string
10
+ readonly directory: string
11
+ readonly tags: ReadonlyArray<string>
12
+ readonly port: number
13
+ readonly bundle: string
14
+ readonly deployedApiUrl?: string
15
+ }
16
+
17
+ type PackageDocument = {
18
+ readonly name?: unknown
19
+ readonly microdot?: unknown
20
+ }
21
+
22
+ const readJson = (path: string): PackageDocument =>
23
+ JSON.parse(readFileSync(path, 'utf8'))
24
+
25
+ const fail = (directory: string, detail: string): never => {
26
+ throw new Error(`[microdots] ${directory}/package.json ${detail}`)
27
+ }
28
+
29
+ export const readMicroDotDirectory = (directory: string): MicroDotPackage => {
30
+ const document = readJson(resolve(directory, 'package.json'))
31
+ const metadata = document.microdot
32
+ if (typeof metadata !== 'object' || metadata === null) {
33
+ return fail(directory, 'must declare a microdot object')
34
+ }
35
+ const tags = 'tags' in metadata ? metadata.tags : undefined
36
+ const port = 'port' in metadata ? metadata.port : undefined
37
+ const bundle = 'bundle' in metadata ? metadata.bundle : undefined
38
+ const deployedApiUrl =
39
+ 'deployedApiUrl' in metadata ? metadata.deployedApiUrl : undefined
40
+ if (
41
+ !Array.isArray(tags) ||
42
+ tags.length === 0 ||
43
+ !tags.every(tag => typeof tag === 'string' && /^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(tag))
44
+ ) {
45
+ return fail(directory, 'microdot.tags must be non-empty custom-element names')
46
+ }
47
+ if (typeof port !== 'number' || !Number.isInteger(port) || port < 1) {
48
+ return fail(directory, 'microdot.port must be a positive integer')
49
+ }
50
+ if (typeof bundle !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*\.js$/.test(bundle)) {
51
+ return fail(directory, 'microdot.bundle must be a kebab-case .js filename')
52
+ }
53
+ if (deployedApiUrl !== undefined && typeof deployedApiUrl !== 'string') {
54
+ return fail(directory, 'microdot.deployedApiUrl must be a string when present')
55
+ }
56
+ return {
57
+ name: basename(directory),
58
+ directory,
59
+ tags,
60
+ port,
61
+ bundle,
62
+ ...(deployedApiUrl === undefined ? {} : { deployedApiUrl }),
63
+ }
64
+ }
65
+
66
+ const assertUnique = (
67
+ dots: ReadonlyArray<MicroDotPackage>,
68
+ label: string,
69
+ values: (dot: MicroDotPackage) => ReadonlyArray<string>,
70
+ ): void => {
71
+ const owners = new Map<string, string>()
72
+ dots.forEach(dot => {
73
+ values(dot).forEach(value => {
74
+ const previous = owners.get(value)
75
+ if (previous !== undefined) {
76
+ throw new Error(`[microdots] duplicate ${label} "${value}" in ${previous} and ${dot.name}`)
77
+ }
78
+ owners.set(value, dot.name)
79
+ })
80
+ })
81
+ }
82
+
83
+ export const listMicroDots = (
84
+ root: string = microdotsRoot,
85
+ ): ReadonlyArray<MicroDotPackage> => {
86
+ if (!existsSync(root)) return []
87
+ const dots = readdirSync(root, { withFileTypes: true })
88
+ .filter(entry => entry.isDirectory())
89
+ .map(entry => resolve(root, entry.name))
90
+ .filter(directory => existsSync(resolve(directory, 'package.json')))
91
+ .map(readMicroDotDirectory)
92
+ .sort((left, right) => left.name.localeCompare(right.name))
93
+ assertUnique(dots, 'tag', dot => dot.tags)
94
+ assertUnique(dots, 'port', dot => [String(dot.port)])
95
+ assertUnique(dots, 'bundle', dot => [dot.bundle])
96
+ return dots
97
+ }
98
+
99
+ export const registryFor = (
100
+ dots: ReadonlyArray<MicroDotPackage> = listMicroDots(),
101
+ ) =>
102
+ dots.flatMap(dot =>
103
+ dot.tags.map(tag => ({
104
+ tag,
105
+ bundle: `/microdots/${dot.bundle}`,
106
+ localPort: dot.port,
107
+ ...(dot.deployedApiUrl === undefined
108
+ ? {}
109
+ : { deployedApiUrl: dot.deployedApiUrl }),
110
+ attributes: {},
111
+ })),
112
+ )
113
+
114
+ export const assertSurfaceTagsMatch = (
115
+ dot: MicroDotPackage,
116
+ surfaces: ReadonlyArray<{ readonly tag: string }>,
117
+ ): void => {
118
+ const declared = [...dot.tags].sort()
119
+ const emitted = surfaces.map(surface => surface.tag).sort()
120
+ if (JSON.stringify(declared) !== JSON.stringify(emitted)) {
121
+ throw new Error(
122
+ `[manifest] ${dot.name} surface tags [${emitted.join(', ')}] do not match package.json microdot.tags [${declared.join(', ')}]`,
123
+ )
124
+ }
125
+ }
126
+
127
+ const VIRTUAL_REGISTRY = 'virtual:microdots-registry'
128
+ const RESOLVED_REGISTRY = `\0${VIRTUAL_REGISTRY}`
129
+
130
+ export const microDotRegistryPlugin = (): Plugin => ({
131
+ name: 'starter:microdot-registry',
132
+ resolveId: id => (id === VIRTUAL_REGISTRY ? RESOLVED_REGISTRY : undefined),
133
+ load: id =>
134
+ id === RESOLVED_REGISTRY
135
+ ? `export const registry = ${JSON.stringify(registryFor())}`
136
+ : undefined,
137
+ })
@@ -0,0 +1,28 @@
1
+ import { resolve } from 'node:path'
2
+ import type { Plugin, UserConfig } from 'vite'
3
+
4
+ import { emitMicroDotManifest } from '@bespokeagentics/microdots-vite-plugin'
5
+
6
+ import { readMicroDotDirectory } from './discover.ts'
7
+
8
+ export const dotConfig = (
9
+ dotDirectory: string,
10
+ plugins: ReadonlyArray<Plugin> = [],
11
+ ): UserConfig => {
12
+ const dot = readMicroDotDirectory(dotDirectory)
13
+ return {
14
+ plugins: [...plugins, emitMicroDotManifest(dotDirectory)],
15
+ build: {
16
+ outDir: resolve(dotDirectory, '../../host/public/microdots'),
17
+ emptyOutDir: false,
18
+ cssCodeSplit: false,
19
+ target: 'esnext',
20
+ lib: {
21
+ entry: resolve(dotDirectory, 'src/entry.ts'),
22
+ formats: ['es'],
23
+ fileName: () => dot.bundle,
24
+ },
25
+ rolldownOptions: { output: { codeSplitting: false } },
26
+ },
27
+ }
28
+ }
@@ -0,0 +1,154 @@
1
+ import { createHash } from 'node:crypto'
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ writeFileSync,
8
+ } from 'node:fs'
9
+ import { resolve } from 'node:path'
10
+ import { pathToFileURL } from 'node:url'
11
+ import { gzipSync } from 'node:zlib'
12
+ import type {
13
+ AttributeSpec,
14
+ MicroDotSurface,
15
+ } from '@bespokeagentics/microdots-element'
16
+
17
+ import {
18
+ assertSurfaceTagsMatch,
19
+ listMicroDots,
20
+ repoRoot,
21
+ } from './discover.ts'
22
+
23
+ const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
24
+ typeof value === 'object' && value !== null
25
+
26
+ const isSurface = (value: unknown): value is MicroDotSurface => {
27
+ if (!isRecord(value)) return false
28
+ return (
29
+ typeof value['tag'] === 'string' &&
30
+ isRecord(value['attributes']) &&
31
+ isRecord(value['events'])
32
+ )
33
+ }
34
+
35
+ const buildId = (bytes: Uint8Array): string =>
36
+ createHash('sha256').update(bytes).digest('hex').slice(0, 8)
37
+
38
+ const manifestAttribute = (name: string, spec: AttributeSpec) => ({
39
+ name,
40
+ type: spec.type ?? 'string',
41
+ required: spec.required ?? false,
42
+ ...(spec.default === undefined ? {} : { default: spec.default }),
43
+ live: spec.live ?? false,
44
+ ownership: spec.ownership ?? 'dot',
45
+ ...(spec.values === undefined ? {} : { values: spec.values }),
46
+ ...(spec.description === undefined ? {} : { description: spec.description }),
47
+ })
48
+
49
+ const eventRef = (
50
+ dotName: string,
51
+ eventName: string,
52
+ payload: unknown,
53
+ contract: Readonly<Record<string, unknown>>,
54
+ ): string => {
55
+ const exportName = Object.keys(contract).find(name =>
56
+ Object.is(contract[name], payload),
57
+ )
58
+ if (exportName === undefined) {
59
+ throw new Error(
60
+ `[manifest] ${dotName}.${eventName} payload is not a named contract export`,
61
+ )
62
+ }
63
+ return `@starter/${dotName}/contract#${exportName}`
64
+ }
65
+
66
+ const assertNoDocumentReset = (name: string, source: string): void => {
67
+ const fingerprints = [
68
+ /\*,\s*::?after,\s*::?before/,
69
+ /(^|[;}]|\\)h1,h2,h3,h4,h5,h6\{/,
70
+ /-webkit-tap-highlight-color/,
71
+ ]
72
+ if (fingerprints.some(pattern => pattern.test(source))) {
73
+ throw new Error(
74
+ `[manifest] ${name} bundle carries a document-wide reset; use the scoped MicroDot Tailwind prologue`,
75
+ )
76
+ }
77
+ }
78
+
79
+ const onlyIndex = process.argv.indexOf('--only')
80
+ const only = onlyIndex === -1 ? undefined : process.argv[onlyIndex + 1]
81
+ const dots = listMicroDots().filter(dot => only === undefined || dot.name === only)
82
+
83
+ if (only !== undefined && dots.length !== 1) {
84
+ throw new Error(`[manifest] no MicroDot named ${only}`)
85
+ }
86
+
87
+ for (const dot of dots) {
88
+ const bundlePath = resolve(repoRoot, 'host/public/microdots', dot.bundle)
89
+ if (!existsSync(bundlePath)) {
90
+ throw new Error(`[manifest] ${dot.name} bundle is missing: ${bundlePath}`)
91
+ }
92
+ const source = readFileSync(bundlePath)
93
+ assertNoDocumentReset(dot.name, source.toString('utf8'))
94
+ const build = buildId(source)
95
+ const stem = dot.bundle.slice(0, -3)
96
+ const hashedFile = `${stem}.${build}.js`
97
+ const dist = resolve(dot.directory, 'dist')
98
+ mkdirSync(dist, { recursive: true })
99
+ copyFileSync(bundlePath, resolve(dist, hashedFile))
100
+
101
+ const surfaceModule: unknown = await import(
102
+ pathToFileURL(resolve(dot.directory, 'src/surface.ts')).href
103
+ )
104
+ const contractModule: unknown = await import(
105
+ pathToFileURL(resolve(dot.directory, 'src/contract.ts')).href
106
+ )
107
+ if (!isRecord(surfaceModule) || !Array.isArray(surfaceModule['surfaces'])) {
108
+ throw new Error(`[manifest] ${dot.name} surface.ts must export surfaces[]`)
109
+ }
110
+ if (!isRecord(contractModule)) {
111
+ throw new Error(`[manifest] ${dot.name} contract exports are unreadable`)
112
+ }
113
+ const surfaces = surfaceModule['surfaces']
114
+ if (!surfaces.every(isSurface)) {
115
+ throw new Error(`[manifest] ${dot.name} surfaces[] contains an invalid surface`)
116
+ }
117
+ assertSurfaceTagsMatch(dot, surfaces)
118
+ const document = {
119
+ name: dot.name,
120
+ build,
121
+ bundle: {
122
+ file: dot.bundle,
123
+ hashedFile,
124
+ bytes: source.byteLength,
125
+ gzipBytes: gzipSync(source).byteLength,
126
+ },
127
+ service: {
128
+ localPort: dot.port,
129
+ ...(dot.deployedApiUrl === undefined
130
+ ? {}
131
+ : { deployedApiUrl: dot.deployedApiUrl }),
132
+ },
133
+ tags: surfaces.map(surface => ({
134
+ tag: surface.tag,
135
+ attributes: Object.entries(surface.attributes).map(([name, spec]) =>
136
+ manifestAttribute(name, spec),
137
+ ),
138
+ events: Object.entries(surface.events).map(([name, event]) => ({
139
+ name,
140
+ payload: {
141
+ ref: eventRef(dot.name, name, event.payload, contractModule),
142
+ },
143
+ ...(event.description === undefined
144
+ ? {}
145
+ : { description: event.description }),
146
+ })),
147
+ })),
148
+ }
149
+ writeFileSync(
150
+ resolve(dist, `${dot.name}.manifest.json`),
151
+ `${JSON.stringify(document, null, 2)}\n`,
152
+ )
153
+ console.log(`[manifest] ${dot.name}@${build} → dist/${dot.name}.manifest.json`)
154
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ESNext", "DOM"],
6
+ "moduleResolution": "bundler",
7
+ "resolveJsonModule": true,
8
+ "allowImportingTsExtensions": true,
9
+ "strict": true,
10
+ "noUncheckedIndexedAccess": true,
11
+ "exactOptionalPropertyTypes": true,
12
+ "isolatedModules": true,
13
+ "skipLibCheck": true,
14
+ "noEmit": true,
15
+ "types": ["bun", "node", "vite/client"]
16
+ },
17
+ "include": ["types.d.ts", "host/**/*", "microdots/**/*", "scripts/**/*", "tests/**/*"]
18
+ }
@@ -0,0 +1,9 @@
1
+ declare module '*.css?inline' {
2
+ const css: string
3
+ export default css
4
+ }
5
+
6
+ declare module 'virtual:microdots-registry' {
7
+ import type { MicroDotRegistry } from '@bespokeagentics/microdots-host'
8
+ export const registry: MicroDotRegistry
9
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'happy-dom',
6
+ include: ['**/*.test.ts'],
7
+ },
8
+ })
@@ -0,0 +1,46 @@
1
+ * { box-sizing: border-box; }
2
+ [hidden] { display: none !important; }
3
+
4
+ html { background: var(--surface-page); color: var(--text-primary); }
5
+ body {
6
+ margin: 0;
7
+ min-width: 20rem;
8
+ min-height: 100vh;
9
+ font-family: var(--font-body);
10
+ background: var(--surface-page);
11
+ }
12
+
13
+ .shell-header,
14
+ .shell-nav,
15
+ main,
16
+ .shell-footer { width: min(72rem, calc(100% - 2rem)); margin-inline: auto; }
17
+
18
+ .shell-header {
19
+ display: flex;
20
+ flex-wrap: wrap;
21
+ align-items: end;
22
+ justify-content: space-between;
23
+ gap: 1rem;
24
+ padding-block: 2.5rem 1.5rem;
25
+ }
26
+ .shell-header h1 { margin: 0; font-size: clamp(1.8rem, 5vw, 3.4rem); line-height: 1.05; }
27
+ .shell-kicker { margin: 0 0 0.5rem; color: var(--text-accent); font-weight: var(--fw-semibold); text-transform: uppercase; letter-spacing: var(--ls-wider); font-size: var(--fs-caption); }
28
+ .theme-control { display: grid; gap: 0.35rem; color: var(--text-secondary); font-size: var(--fs-label); }
29
+ .theme-control select { color: var(--text-primary); background: var(--surface-default); border: 1px solid var(--border-default); border-radius: var(--radius-sm); padding: 0.6rem 2rem 0.6rem 0.75rem; }
30
+
31
+ .shell-nav { display: flex; flex-wrap: wrap; gap: 0.4rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--border-subtle); }
32
+ .shell-nav__link { color: var(--text-secondary); text-decoration: none; border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; }
33
+ .shell-nav__link:hover { color: var(--text-primary); background: var(--int-hover); }
34
+ .shell-nav__link[aria-current='page'] { color: var(--text-accent); background: var(--int-active); font-weight: var(--fw-semibold); }
35
+
36
+ main { padding-block: 1.5rem 3rem; }
37
+ .route-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: 1rem; align-items: start; }
38
+ .route-section { display: contents; }
39
+ .microdot-slot { min-width: 0; }
40
+ .microdot-slot > * { display: block; }
41
+
42
+ .placeholder { min-height: 14rem; display: grid; place-content: center; gap: 0.5rem; padding: 2rem; text-align: center; color: var(--text-secondary); background: var(--surface-default); border: 1px dashed var(--border-default); border-radius: var(--radius-lg); }
43
+ .placeholder h2, .placeholder p { margin: 0; }
44
+ .placeholder--error { color: var(--status-error-fg); border-color: var(--status-error-border); background: var(--status-error-bg); }
45
+
46
+ .shell-footer { padding-block: 1rem 2rem; color: var(--text-muted); border-top: 1px solid var(--border-subtle); font-family: var(--font-mono); font-size: var(--fs-mono-sm); }
@@ -0,0 +1,4 @@
1
+ import { defineConfig } from 'vite'
2
+ import { hostConfig } from './vite.base.ts'
3
+
4
+ export default defineConfig(hostConfig())
@@ -0,0 +1,17 @@
1
+ @import './tokens.css';
2
+
3
+ .starter-card { font-family: var(--font-body); color: var(--text-primary); background: var(--surface-default); border: 1px solid var(--border-default); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); padding: 1.25rem; display: grid; gap: 1rem; }
4
+ .starter-card h2 { margin: 0; color: var(--text-heading); font-family: var(--font-display); font-size: var(--fs-display-m); }
5
+ .starter-card header, .starter-card footer { display: grid; gap: 0.5rem; }
6
+ .starter-card footer { grid-template-columns: 1fr auto; align-items: center; border-top: 1px solid var(--border-subtle); padding-top: 1rem; color: var(--text-muted); font-size: var(--fs-caption); }
7
+ .starter-card__body { min-height: 8rem; display: grid; align-content: center; gap: 0.75rem; padding: 1rem; background: var(--surface-raised); border-radius: var(--radius-md); }
8
+ .microdot-overline, .microdot-note, .microdot-state { margin: 0; }
9
+ .microdot-overline { color: var(--text-muted); font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: 0.08em; }
10
+ .microdot-note { color: var(--text-secondary); font-size: var(--fs-body-s); line-height: 1.5; }
11
+ .microdot-state { color: var(--text-primary); font-weight: var(--fw-medium); }
12
+ .microdot-state--error { color: var(--status-error-fg); }
13
+ .microdot-action { font: inherit; cursor: pointer; color: var(--text-on-brand); background: var(--accent-fill); border: 0; border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; }
14
+ .microdot-action:hover { background: var(--accent-fill-hover); }
15
+ .pulse__field { display: flex; justify-content: space-between; gap: 1rem; }
16
+ .pulse__label { color: var(--text-muted); font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: 0.05em; }
17
+ .pulse__value { color: var(--text-primary); font-family: var(--font-mono); font-size: var(--fs-caption); text-align: right; word-break: break-all; }
@@ -0,0 +1,4 @@
1
+ import { defineConfig } from 'vite'
2
+ import { dotConfig } from '../../scripts/dot-vite.ts'
3
+
4
+ export default defineConfig(dotConfig(import.meta.dirname))
@@ -0,0 +1,19 @@
1
+ @import './tokens.css';
2
+
3
+ .starter-card { font-family: var(--font-body); color: var(--text-primary); background: var(--surface-default); border: 1px solid var(--border-default); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); padding: 1.25rem; display: grid; gap: 1rem; }
4
+ .starter-card h2 { margin: 0; color: var(--text-heading); font-family: var(--font-display); font-size: var(--fs-display-m); }
5
+ .starter-card header, .starter-card footer { display: grid; gap: 0.5rem; }
6
+ .starter-card footer { grid-template-columns: 1fr auto; align-items: center; border-top: 1px solid var(--border-subtle); padding-top: 1rem; color: var(--text-muted); font-size: var(--fs-caption); }
7
+ .starter-card__body { min-height: 8rem; display: grid; align-content: center; gap: 0.75rem; padding: 1rem; background: var(--surface-raised); border-radius: var(--radius-md); }
8
+ .microdot-overline, .microdot-note, .microdot-state { margin: 0; }
9
+ .microdot-overline { color: var(--text-muted); font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: 0.08em; }
10
+ .microdot-note { color: var(--text-secondary); font-size: var(--fs-body-s); line-height: 1.5; }
11
+ .microdot-state { color: var(--text-primary); font-weight: var(--fw-medium); }
12
+ .microdot-state--error { color: var(--status-error-fg); }
13
+ .microdot-action, .scope-picker__choice { font: inherit; cursor: pointer; border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; }
14
+ .microdot-action { color: var(--text-on-brand); background: var(--accent-fill); border: 0; }
15
+ .microdot-action:hover { background: var(--accent-fill-hover); }
16
+ .scope-picker__choices { display: flex; flex-wrap: wrap; gap: 0.5rem; }
17
+ .scope-picker__choice { color: var(--text-primary); background: var(--surface-default); border: 1px solid var(--border-default); }
18
+ .scope-picker__choice:hover { background: var(--int-hover); }
19
+ .scope-picker__choice--selected { background: var(--int-active); border-color: var(--accent-fill); }
@@ -0,0 +1,4 @@
1
+ import { defineConfig } from 'vite'
2
+ import { dotConfig } from '../../scripts/dot-vite.ts'
3
+
4
+ export default defineConfig(dotConfig(import.meta.dirname))
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "workspaces": ["host", "microdots/*"],
7
+ "scripts": {
8
+ "dev": "bun scripts/dev.ts",
9
+ "build": "bun scripts/build.ts",
10
+ "typecheck": "tsc --noEmit",
11
+ "lint": "oxlint host microdots scripts",
12
+ "test": "vitest run",
13
+ "tokens:check": "bun scripts/check-tokens.ts",
14
+ "check": "bun run typecheck && bun run lint && bun run test && bun run tokens:check"
15
+ },
16
+ "dependencies": {
17
+ "@bespokeagentics/microdots-authoring": "0.1.1",
18
+ "@bespokeagentics/microdots-element": "0.1.1",
19
+ "@bespokeagentics/microdots-host": "0.1.1",
20
+ "@bespokeagentics/microdots-runtime": "0.1.1",
21
+ "@bespokeagentics/microdots-theme": "0.1.1",
22
+ "@effect/platform-node": "4.0.0-rc.108",
23
+ "effect": "4.0.0-rc.108",
24
+ "foldkit": "^0.144.0"
25
+ },
26
+ "devDependencies": {
27
+ "@bespokeagentics/microdots-vite-plugin": "0.1.1",
28
+ "@types/bun": "^1.3.14",
29
+ "@types/node": "^26.2.0",
30
+ "happy-dom": "^20.10.4",
31
+ "oxlint": "^1.78.0",
32
+ "typescript": "^7.0.2",
33
+ "vite": "^8.0.16",
34
+ "vitest": "^4.1.10"
35
+ },
36
+ "overrides": {
37
+ "@effect/platform-node-shared": "4.0.0-rc.108"
38
+ }
39
+ }
@@ -0,0 +1,28 @@
1
+ @import 'tailwindcss';
2
+ @import '@bespokeagentics/microdots-theme/theme.css';
3
+ @source './';
4
+
5
+ [hidden] { display: none !important; }
6
+
7
+ @layer components {
8
+ html { @apply bg-surface-page text-fg; }
9
+ body { @apply m-0 min-h-screen min-w-80 bg-surface-page font-body; }
10
+ .shell-header, .shell-nav, main, .shell-footer { @apply mx-auto w-[min(72rem,calc(100%-2rem))]; }
11
+ .shell-header { @apply flex flex-wrap items-end justify-between gap-4 py-10 pb-6; }
12
+ .shell-header h1 { @apply m-0 text-[clamp(1.8rem,5vw,3.4rem)] leading-tight; }
13
+ .shell-kicker { @apply mb-2 mt-0 text-caption font-semibold uppercase tracking-wider text-fg-accent; }
14
+ .theme-control { @apply grid gap-1.5 text-label text-fg-muted; }
15
+ .theme-control select { @apply rounded-sm border border-border bg-surface px-3 py-2 text-fg; }
16
+ .shell-nav { @apply flex flex-wrap gap-1.5 border-b border-border-subtle pb-6; }
17
+ .shell-nav__link { @apply rounded-sm px-3 py-2 text-fg-muted no-underline hover:bg-int-hover hover:text-fg; }
18
+ .shell-nav__link[aria-current='page'] { @apply bg-int-active font-semibold text-fg-accent; }
19
+ main { @apply py-6 pb-12; }
20
+ .route-grid { @apply grid items-start gap-4; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); }
21
+ .route-section { display: contents; }
22
+ .microdot-slot { @apply min-w-0; }
23
+ .microdot-slot > * { @apply block; }
24
+ .placeholder { @apply grid min-h-56 place-content-center gap-2 rounded-lg border border-dashed border-border bg-surface p-8 text-center text-fg-muted; }
25
+ .placeholder h2, .placeholder p { @apply m-0; }
26
+ .placeholder--error { @apply border-status-error-border bg-status-error-bg text-status-error-fg; }
27
+ .shell-footer { @apply border-t border-border-subtle py-4 pb-8 font-mono text-mono-sm text-fg-subtle; }
28
+ }
@@ -0,0 +1,5 @@
1
+ import tailwindcss from '@tailwindcss/vite'
2
+ import { defineConfig } from 'vite'
3
+ import { hostConfig } from './vite.base.ts'
4
+
5
+ export default defineConfig(hostConfig(tailwindcss()))