@estiva-app/ui 0.48.0 → 0.49.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.
@@ -0,0 +1,35 @@
1
+ import type { Rule } from 'eslint';
2
+ /**
3
+ * The parts a person types into to send something. `Field` is the labelled
4
+ * row; `TextInput` and `Textarea` are judged on their own only in a file that
5
+ * draws no `Field`, so a field is reported once, at its label.
6
+ */
7
+ export declare const FIELD_PARTS: readonly ["Field", "TextInput", "Textarea"];
8
+ /** The parts that are a `Form`: the package's own, and the palette's form level, which is one (UIG-29). */
9
+ export declare const FORM_PARTS: readonly ["Form", "CommandPaletteForm"];
10
+ /**
11
+ * A field outside a `Form` (Katerina, 1 October, after Peek's and Ship's
12
+ * Archive dialog: a resolution typed into a `ConfirmDialog`, sent by its
13
+ * button, with no form around it — Ctrl+Enter did nothing, and the field
14
+ * stayed open while it archived).
15
+ *
16
+ * `Form` owns what a field needs to be sent: Enter and Ctrl+Enter, `busy`
17
+ * switching every field off at once, and where focus goes after. A field
18
+ * outside one has none of it, and the dialog around it is the wrong one: a
19
+ * question with a field in it is a `DialogShell` with a `Form`
20
+ * (ConfirmDialog's page, **When not**).
21
+ *
22
+ * Judged: a `Field`, a `TextInput` or a `Textarea` of the package (or an
23
+ * app's re-export or wrapper of one) with no `Form` around it in the same
24
+ * file. Not judged, on purpose:
25
+ *
26
+ * - `EditableText` and `SearchInput`: one saves itself as you leave it, the
27
+ * other filters a list, and neither is sent (Form's page, **When not**);
28
+ * - a `TextInput` or `Textarea` in a file that draws a `Field`: the `Field`
29
+ * is reported, and a control handed to it from a function of its own (a
30
+ * `switch` over field kinds) cannot be followed to it;
31
+ * - the app's own components: a component that draws a field for a form its
32
+ * caller holds says so with an escape on the field, naming that caller.
33
+ */
34
+ export declare const noFieldOutsideAForm: Rule.RuleModule;
35
+ //# sourceMappingURL=no-field-outside-a-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-field-outside-a-form.d.ts","sourceRoot":"","sources":["../../src/eslint/no-field-outside-a-form.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAIlC;;;;GAIG;AACH,eAAO,MAAM,WAAW,6CAA8C,CAAA;AAEtE,2GAA2G;AAC3G,eAAO,MAAM,UAAU,yCAA0C,CAAA;AAMjE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB,EAAE,IAAI,CAAC,UAiDtC,CAAA"}
@@ -11,7 +11,7 @@
11
11
  */
12
12
  /** The name the configs register the plugin under, so every rule id is `estiva/<rule>`. */
13
13
  export declare const PLUGIN_KEY = "estiva";
14
- export declare const APP_RULE_NAMES: readonly ["no-raw-element", "no-rebuilt-behaviour", "no-restyled-part", "no-handmade-header", "no-handmade-empty-state", "no-native-title", "no-copied-look"];
14
+ export declare const APP_RULE_NAMES: readonly ["no-raw-element", "no-rebuilt-behaviour", "no-restyled-part", "no-handmade-header", "no-handmade-empty-state", "no-native-title", "no-copied-look", "no-field-outside-a-form"];
15
15
  export type AppRuleName = (typeof APP_RULE_NAMES)[number];
16
16
  /** The ids `recommended` and `strict` carry — what an app's gate runs and counts. */
17
17
  export declare const APP_RULE_IDS: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"rule-ids.d.ts","sourceRoot":"","sources":["../../src/eslint/rule-ids.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,2FAA2F;AAC3F,eAAO,MAAM,UAAU,WAAW,CAAA;AAElC,eAAO,MAAM,cAAc,+JAQjB,CAAA;AAEV,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAAwD,CAAA"}
1
+ {"version":3,"file":"rule-ids.d.ts","sourceRoot":"","sources":["../../src/eslint/rule-ids.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,2FAA2F;AAC3F,eAAO,MAAM,UAAU,WAAW,CAAA;AAElC,eAAO,MAAM,cAAc,0LASjB,CAAA;AAEV,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAAwD,CAAA"}
@@ -19,7 +19,8 @@ var APP_RULE_NAMES = [
19
19
  "no-handmade-header",
20
20
  "no-handmade-empty-state",
21
21
  "no-native-title",
22
- "no-copied-look"
22
+ "no-copied-look",
23
+ "no-field-outside-a-form"
23
24
  ];
24
25
  var APP_RULE_IDS = APP_RULE_NAMES.map((name) => `${PLUGIN_KEY}/${name}`);
25
26
 
@@ -1048,4 +1049,4 @@ export {
1048
1049
  askNpm,
1049
1050
  createApp
1050
1051
  };
1051
- //# sourceMappingURL=chunk-DZVDOJGV.js.map
1052
+ //# sourceMappingURL=chunk-FIBZLYQI.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/gates/create-app.ts", "../../src/eslint/rule-ids.ts"],
4
- "sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `create-estiva-app <name>` \u2014 a new Estiva app that runs, with every gate on\n * (UIG-10).\n *\n * npx -p @estiva-app/ui create-estiva-app leaf [--title Leaf] [--theme light]\n *\n * The command is `create-app-cli.ts`; this file is what it runs, and what a test reads.\n *\n * **It reads only this package** (Katerina's ruling of 17 September, docs/GATES.md\n * \u00A723). Never Peek, never Ship: they are private, and reading them would carry\n * their history into every new app. What a new app needs to run a gate is this\n * package's own \u2014 the rules, the token and gate configs, the hook, the count,\n * gates:status and the checks every app runs \u2014 so the app gets short files that\n * import them, and a gate added later arrives with a version bump.\n *\n * What only a new app has is written here: its first page in the sidebar frame\n * (ruled the same day), its one theme, sign-in the way Ship signs in, its one\n * relay connection the way Peek and Ship hold theirs, its tests, its stories,\n * the CI workflow with the job `gate`, and its README and CLAUDE.md.\n *\n * The relay (UIG-10, reopened 18 September): a made app gets `protocol`,\n * `platform` and `interop` and is connected from its first commit, or runs alone\n * when no relay is set. The wiring is the app's own file, using `platform` as it\n * is \u2014 not a helper in `platform` \u2014 because a package takes code a real app has\n * already run (ADR 0002 \u00A710), and Peek and Ship each hold theirs the same way.\n *\n * Tool versions are this package's own (its devDependencies build and test the\n * same tools). The ones it does not use itself ({@link ASKED_OF_NPM}) are asked of\n * the npm registry when the app is made. The lockfile is the app's first\n * `npm install`; make it on Linux.\n */\nimport { execFileSync } from 'node:child_process'\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { APP_RULE_IDS } from '../eslint/rule-ids'\nimport { loaderText, SEARCH_RULES } from '../registry/skill'\n\ninterface PackageJson {\n version: string\n devDependencies: Record<string, string>\n}\n\nexport interface CreateAppOptions {\n /** The folder and npm name: lowercase letters, digits and dashes. */\n name: string\n /** What the app is called on screen. */\n title?: string\n /** One of this package's themes. */\n theme?: string\n /** The folder the app's folder is made in. */\n parent?: string\n /** The `@estiva-app/ui` dependency, when not this package's own version (a tarball, for a proof). */\n ui?: string\n /** Versions of the packages this package does not itself use; asked of npm when absent. */\n versions?: Record<string, string>\n}\n\nconst here = dirname(fileURLToPath(import.meta.url))\nconst packageRoot = resolve(here, '..', '..')\n\n/** What a made app depends on that this package does not use itself: asked of npm when the app is made. */\nexport const ASKED_OF_NPM = ['@estiva-app/identity', '@estiva-app/interop', '@estiva-app/platform', '@estiva-app/protocol', 'eslint-plugin-react-hooks']\n\n/** The package's themes, read from its tokens.css: `light` is the block with no attribute. */\nexport function themes(): string[] {\n const css = readFileSync(join(packageRoot, 'tokens.css'), 'utf8')\n return ['light', ...new Set([...css.matchAll(/data-theme='([\\w-]+)'/g)].map((m) => m[1]))]\n}\n\n/** Every file a new app starts with, by path. Nothing is written. */\nexport function appFiles({ name, title = name, theme = 'light', ui, versions = {} }: CreateAppOptions): Record<string, string> {\n if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`\"${name}\" is not a name an app can have: lowercase letters, digits and dashes, starting with a letter`)\n const known = themes()\n if (!known.includes(theme)) throw new Error(`\"${theme}\" is not one of the package's themes: ${known.join(', ')}`)\n\n const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')) as PackageJson\n const own = (dep: string) => {\n const range = pkg.devDependencies[dep] ?? versions[dep]\n if (!range) throw new Error(`no version for ${dep}`)\n return range\n }\n const deps = (names: string[]) => Object.fromEntries(names.map((n) => [n, own(n)]))\n\n const packageJson = {\n name,\n private: true,\n version: '0.0.0',\n type: 'module',\n scripts: {\n dev: 'vite',\n build: 'tsc -b && vite build',\n preview: 'vite preview',\n typecheck: 'tsc -b',\n lint: 'eslint .',\n 'lint:tokens': 'eslint --config eslint.tokens.config.js .',\n 'lint:rules': 'eslint --config eslint.gates.config.js .',\n 'postlint:rules': `estiva-gates count --repo ${name}`,\n 'gates:status': 'estiva-gates status',\n 'ui:find': 'estiva-ui find',\n registry: 'estiva-ui build',\n 'registry:check': 'estiva-ui check',\n test: 'vitest run',\n storybook: 'storybook dev -p 6006',\n 'build-storybook': 'storybook build',\n },\n dependencies: {\n '@estiva-app/identity': own('@estiva-app/identity'),\n '@estiva-app/interop': own('@estiva-app/interop'),\n '@estiva-app/platform': own('@estiva-app/platform'),\n '@estiva-app/protocol': own('@estiva-app/protocol'),\n '@estiva-app/ui': ui ?? `^${pkg.version}`,\n ...deps(['@tabler/icons-react', 'react', 'react-dom']),\n },\n devDependencies: deps([\n '@eslint/js', '@storybook/addon-docs', '@storybook/react-vite', '@testing-library/dom', '@testing-library/react',\n '@types/node', '@types/react', '@types/react-dom', '@vitejs/plugin-react', 'autoprefixer', 'eslint',\n 'eslint-plugin-better-tailwindcss', 'eslint-plugin-react-hooks', 'globals', 'jsdom', 'postcss', 'storybook',\n 'tailwindcss', 'typescript', 'typescript-eslint', 'vite', 'vitest',\n ]),\n }\n\n const count = { schemaVersion: 1, repo: name, generatedAt: new Date().toISOString(), rules: Object.fromEntries(APP_RULE_IDS.map((id) => [id, { errors: 0, warnings: 0, escapes: 0 }])) }\n const themeAttr = ` data-theme=\"${theme}\"`\n const tsBase = {\n target: 'ES2023',\n module: 'ESNext',\n skipLibCheck: true,\n moduleResolution: 'bundler',\n allowImportingTsExtensions: true,\n verbatimModuleSyntax: true,\n moduleDetection: 'force',\n noEmit: true,\n strict: true,\n noUnusedLocals: false,\n noUnusedParameters: false,\n erasableSyntaxOnly: true,\n noFallthroughCasesInSwitch: true,\n noUncheckedSideEffectImports: true,\n }\n const json = (value: unknown) => `${JSON.stringify(value, null, 2)}\\n`\n\n return {\n 'package.json': json(packageJson),\n\n // registry.json: the app's catalogue, written only when `npm run registry` is asked\n // to. It is built fresh from the code every time it is read, so it is never kept.\n '.gitignore': ['node_modules', 'dist', 'storybook-static', '*.local', '*.log', '*.tsbuildinfo', '.DS_Store', 'registry.json', ''].join('\\n'),\n\n '.env.example': `# Estiva ID, for signing in. Copy this file to .env.local and fill both in.\n#\n# Left empty, the app offers no sign-in at all and runs anonymous: it never\n# reaches the real Estiva ID by accident.\n#\n# The app must be registered with that Estiva ID first, as its own app, with\n# this redirect address: the origin the app runs on, and a slash\n# (http://localhost:5173/). The real Estiva ID registers a new app on the\n# server; a local one (http://localhost:8787) in its database.\nVITE_ESTIVA_ID_ORIGIN=\nVITE_ESTIVA_ID_CLIENT_ID=${name}\n\n# The relay: the workspace this app reads and writes.\n#\n# Left empty, the app runs alone. It opens no connection, and its home page says\n# so. It also needs sign-in above: the relay refuses anything before sign-in, so\n# with a relay and no sign-in there is still no connection. And Estiva ID must\n# allow this app to sign in to that relay (the README says how).\n#\n# A relay running on this machine is http://localhost:3000.\nVITE_RELAY_URL=\n`,\n\n 'index.html': `<!doctype html>\n<html lang=\"en\"${themeAttr}>\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${title}</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"/src/main.tsx\"></script>\n </body>\n</html>\n`,\n\n 'tsconfig.json': json({ files: [], references: [{ path: './tsconfig.app.json' }, { path: './tsconfig.node.json' }] }),\n 'tsconfig.app.json': json({\n compilerOptions: { tsBuildInfoFile: './node_modules/.tmp/tsconfig.app.tsbuildinfo', ...tsBase, useDefineForClassFields: true, lib: ['ES2023', 'DOM', 'DOM.Iterable'], types: ['vite/client'], jsx: 'react-jsx', paths: { '@/*': ['./src/*'] } },\n include: ['src', '.storybook'],\n }),\n 'tsconfig.node.json': json({\n compilerOptions: { tsBuildInfoFile: './node_modules/.tmp/tsconfig.node.tsbuildinfo', ...tsBase, lib: ['ES2023'], types: ['node'] },\n include: ['vite.config.ts'],\n }),\n\n 'vite.config.ts': `import { fileURLToPath } from 'node:url'\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vitest/config'\n\n// \\`react\\` is deduped because @estiva-app/ui declares it a peer: two copies in one\n// tree is the \"invalid hook call\" crash, and it appears at runtime.\nexport default defineConfig({\n plugins: [react()],\n resolve: {\n alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },\n dedupe: ['react', 'react-dom'],\n },\n test: {\n environment: 'jsdom',\n },\n})\n`,\n\n 'tailwind.config.js': `import estiva, { estivaContent } from '@estiva-app/ui/tailwind-preset'\n\n/**\n * The package's preset, and nothing of the app's own: a token is added to the\n * package, in every theme, never here.\n *\n * \\`estivaContent\\` is the package's own files. Tailwind does not merge \\`content\\`\n * from a preset, so without it every class used only by a package part is\n * purged, and the app builds clean while rendering at the wrong size.\n *\n * @type {import('tailwindcss').Config}\n */\nexport default {\n presets: [estiva],\n content: [...estivaContent, './index.html', './src/**/*.{ts,tsx}', './.storybook/**/*.{ts,tsx}'],\n}\n`,\n 'postcss.config.js': `export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n`,\n\n 'eslint.config.js': `import js from '@eslint/js'\nimport { gateLint, TOKEN_LINT_IGNORES, tokenLint, tokenValues } from '@estiva-app/ui/gates'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport { defineConfig, globalIgnores } from 'eslint/config'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\n/**\n * Everything, in one lint (\\`npm run lint\\`, a CI step): TypeScript's and React's\n * recommended rules, the token contract and the UI Guardrails' rules. A new app\n * has no backlog, so all of it is a gate from the first commit.\n *\n * The token contract and the gate are the package's (\\`@estiva-app/ui/gates\\`),\n * imported, never copied: a rule written later arrives with a version bump.\n */\nexport default defineConfig([\n globalIgnores(TOKEN_LINT_IGNORES),\n {\n files: ['**/*.{ts,tsx}'],\n extends: [js.configs.recommended, tseslint.configs.recommended, reactHooks.configs.flat.recommended],\n languageOptions: { globals: globals.browser },\n },\n tokenLint(),\n tokenValues(),\n gateLint(),\n])\n`,\n 'eslint.tokens.config.js': `import { tokenConfig } from '@estiva-app/ui/gates'\nimport reactHooks from 'eslint-plugin-react-hooks'\n\n// The token contract on its own (\\`npm run lint:tokens\\`), the package's piece.\n// React's hooks plugin is named so its directives do not break it; TypeScript's\n// the package names itself.\nexport default tokenConfig({ quiet: { 'react-hooks': reactHooks } })\n`,\n 'eslint.gates.config.js': `import { gateConfig } from '@estiva-app/ui/gates'\nimport reactHooks from 'eslint-plugin-react-hooks'\n\n/**\n * The UI Guardrails' rules on their own \u2014 \\`npm run lint:rules\\`, CI's job \\`gate\\`\n * (GitHub requires it on main: never rename it), and what the editor hook in\n * \\`.claude/settings.json\\` lints a proposed write with. The package's gate,\n * imported. React's hooks plugin is named so its directives do not break it;\n * TypeScript's the package names itself.\n *\n * A place that keeps something the gate refuses says why, on the line above:\n * \\`// @estiva-escape(<rule>): <reason>\\`, naming the rule it keeps off. Never\n * \\`eslint-disable\\`: the count refuses it.\n */\nexport default gateConfig({ quiet: { 'react-hooks': reactHooks } })\n`,\n\n '.claude/settings.json': json({\n hooks: {\n PreToolUse: [{ matcher: 'Edit|Write', hooks: [{ type: 'command', command: 'node \"$CLAUDE_PROJECT_DIR/node_modules/@estiva-app/ui/dist/gates/cli.js\" hook' }] }],\n },\n permissions: { allow: SEARCH_RULES },\n }),\n\n // The Claude skill (UIG-20): its trigger, and a line reading the installed package's text in.\n '.claude/skills/estiva-ui/SKILL.md': loaderText(resolve('/app'), resolve('/app/node_modules/@estiva-app/ui')),\n\n 'scripts/gates-checks.mjs': `import { appChecks } from '@estiva-app/ui/gates'\n\n/**\n * What gates:status checks in ${name}: the gate checks every app runs, from the\n * package (\\`appChecks\\`), and nothing of its own yet. A check about this app's\n * own code goes beside them.\n */\nexport default function define(h) {\n return { repo: '${name}', tickets: appChecks(h, { page: 'src/pages/HomePage.tsx' }) }\n}\n`,\n '.gates-count.json': json(count),\n 'docs/GATES-DEBT.md': `# What ${title} owes the gates\n\nNothing. ${title} was made with every gate on, at zero.\n\nIt should stay that way. A place that keeps something the gate refuses says why on\nthe line above it, \\`// @estiva-escape(<rule>): <reason>\\`. This page does not list those:\n\\`npm run gates:status -- --escapes\\` does, from the code, with each one's age, and\n\\`npm run gates:status\\` shows the ones older than 30 days, to keep with a new reason or fix.\n\n## Folders the gate does not read\n\nNone. A folder the gate skips is named here, with why; CI's job \\`gate\\` fails on one\nthat is not.\n`,\n\n '.github/workflows/deploy.yml': `name: deploy\n\n# Every pull request and every push to main runs the checks. There is no deploy\n# job yet: a new app has nowhere to go until it has a home. When it does, the job\n# goes here, and it needs \\`check\\` and \\`gate\\` first.\n\non:\n push:\n branches: [main]\n pull_request:\n\nconcurrency:\n group: deploy-${name}-\\${{ github.ref }}\n cancel-in-progress: true\n\njobs:\n check:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 24\n cache: npm\n - run: npm ci\n - name: Typecheck the app\n run: npx tsc -b tsconfig.app.json\n - name: Typecheck the settings files\n run: npx tsc -b tsconfig.node.json\n - name: Lint \u2014 everything, the token contract included\n run: npm run lint\n - name: Test\n run: npm test\n - name: Build\n run: npm run build\n # A class only a package part uses reaches the stylesheet only if\n # tailwind.config.js spreads estivaContent. When it does not, the build still\n # succeeds and parts render at the wrong size, so this is where it is caught.\n - name: The package's classes survive Tailwind's purge\n run: |\n if ! grep -qr \"max-h-72\" dist/assets/*.css; then\n echo \"::error::a class used only by @estiva-app/ui is missing from the built CSS \u2014 check estivaContent is spread into tailwind.config.js\"\n exit 1\n fi\n\n # The UI Guardrails, the one check a merge requires: the package's gate rules\n # (each refusal names what to use instead), the token contract, and the\n # catalogue. \\`npm run lint:rules\\` also writes .gates-count.json and fails on\n # an eslint-disable that switches a rule off.\n #\n # A job of its own, because GitHub can require only a whole job, by its name:\n # the rule on main requires \\`gate\\`. Renaming it leaves every pull request\n # waiting for a check that never reports.\n gate:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 24\n cache: npm\n - run: npm ci\n # Every step of the gate, the same command Peek and Ship run: the gate\n # lint and \\`.gates-count.json\\` (committed, or this fails), the token\n # contract, the catalogue (every part says in one line what it is for),\n # and the wall's own settings: the editor hook in .claude/settings.json\n # refuses a raw button, and every folder the gate skips is named in\n # docs/GATES-DEBT.md.\n - name: The gate (estiva-gates ci)\n run: npx estiva-gates ci\n`,\n\n 'src/index.css': `@import '@estiva-app/ui/tokens.css';\n@import '@estiva-app/ui/base.css';\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nhtml,\nbody,\n#root {\n height: 100%;\n}\n`,\n 'src/vite-env.d.ts': `/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n readonly VITE_ESTIVA_ID_ORIGIN?: string\n readonly VITE_ESTIVA_ID_CLIENT_ID?: string\n readonly VITE_RELAY_URL?: string\n}\n`,\n 'src/config.ts': `/** What the app is called on screen. */\nexport const APP_TITLE = ${JSON.stringify(title)}\n\n/**\n * Estiva ID, or \\`null\\` when this build offers no sign-in at all. Empty is a real\n * mode, not a broken one: the app runs anonymous, and a local build can never\n * reach the real Estiva ID by accident. See .env.example.\n */\nexport const ID_CONFIG: { base: string; clientId: string } | null =\n import.meta.env.VITE_ESTIVA_ID_ORIGIN && import.meta.env.VITE_ESTIVA_ID_CLIENT_ID\n ? { base: import.meta.env.VITE_ESTIVA_ID_ORIGIN.replace(/\\\\/+$/, ''), clientId: import.meta.env.VITE_ESTIVA_ID_CLIENT_ID }\n : null\n\n/**\n * The relay, the workspace this app reads and writes, or \\`null\\` when this build\n * has none. Empty is a real mode too: the app runs alone and opens no connection,\n * and a local build can never reach the real relay by accident. See .env.example.\n */\nexport const RELAY_URL: string | null = import.meta.env.VITE_RELAY_URL?.trim().replace(/\\\\/+$/, '') || null\n\n/** The relay as a person would name it: host and port, no scheme. */\nexport function relayLabel(url: string): string {\n try {\n return new URL(url).host\n } catch {\n return url\n }\n}\n`,\n 'src/auth/estivaId.ts': `import { createEstivaId, type ShellReason, type StoredToken } from '@estiva-app/identity'\nimport { ID_CONFIG } from '../config'\n\n/**\n * Signing in with Estiva ID, the way Ship does: \\`@estiva-app/identity\\` holds the\n * flow, this file the app's choices.\n *\n * - The redirect is the origin and a slash, never the current path: Estiva ID\n * compares it exactly against the one address registered for the app.\n * - The session lives in localStorage, so it outlives a tab; a sign-in's\n * single-use credentials in sessionStorage, per tab.\n */\nconst client = ID_CONFIG\n ? createEstivaId({\n base: ID_CONFIG.base,\n clientId: ID_CONFIG.clientId,\n redirectUri: () => \\`\\${window.location.origin}/\\`,\n storage: () => (typeof localStorage === 'undefined' ? null : localStorage),\n pendingStore: () => (typeof sessionStorage === 'undefined' ? null : sessionStorage),\n keyPrefix: '${name}.estiva-id',\n navigate: (url) => window.location.assign(url),\n })\n : null\n\nexport const signInAvailable = client !== null\n\nexport function currentToken(): StoredToken | null {\n return client?.validToken(Date.now() + 60_000) ?? null\n}\n\nexport function startRenewal(): void {\n client?.scheduleRenewal()\n}\n\nexport async function beginSignIn(options: { silent?: boolean } = {}): Promise<void> {\n await client?.beginSignIn(\\`\\${window.location.pathname}\\${window.location.search}\\`, options)\n}\n\nexport function beginSignOut(): void {\n client?.beginSignOut()\n}\n\nexport async function completeSignIn(): Promise<{ token: StoredToken; returnTo: string }> {\n if (!client) throw new Error('this build offers no sign-in')\n return client.completeSignIn(window.location.search)\n}\n\nexport function stashShellReason(reason: ShellReason): void {\n client?.stashShellReason(reason)\n}\n\nexport function takeShellReason() {\n return client?.takeShellReason() ?? null\n}\n\nexport function callbackParams(): { code?: string; error?: string } | null {\n const params = new URLSearchParams(window.location.search)\n const code = params.get('code') ?? undefined\n const error = params.get('error') ?? undefined\n return code || error ? { code, error } : null\n}\n\nexport function clearQuery(): void {\n window.history.replaceState({}, '', window.location.pathname)\n}\n\nexport function guardAttemptedAt(): number | null {\n if (typeof sessionStorage === 'undefined') return null\n const raw = sessionStorage.getItem('estiva.authShell.silentAttempt')\n const at = raw ? Number(raw) : NaN\n return Number.isFinite(at) ? at : null\n}\n\n/**\n * Who Estiva ID says this is. Asked of its directory with the token, so the\n * answer also proves Estiva ID accepted the sign-in.\n */\nexport async function whoAmI(): Promise<{ name?: string; email?: string } | null> {\n const token = currentToken()\n if (!ID_CONFIG || !token) return null\n const response = await fetch(\\`\\${ID_CONFIG.base}/directory/\\${token.pubkey}\\`, { headers: { Authorization: \\`Bearer \\${token.accessToken}\\` } })\n if (!response.ok) return null\n const entry = (await response.json()) as { displayName?: string; email?: string }\n return { name: entry.displayName, email: entry.email }\n}\n`,\n 'src/auth/boot.ts': `import { decideBoot, type ShellReason, type ShellState } from '@estiva-app/identity'\nimport {\n beginSignIn,\n callbackParams,\n clearQuery,\n completeSignIn,\n currentToken,\n guardAttemptedAt,\n signInAvailable,\n startRenewal,\n stashShellReason,\n takeShellReason,\n} from './estivaId'\n\nexport type BootResult = { kind: 'app' } | { kind: 'shell'; state: ShellState } | { kind: 'leaving' }\n\n/**\n * Settle who this load belongs to, before the app renders. The decision is\n * \\`decideBoot\\` from \\`@estiva-app/identity\\`; this is the reading and acting\n * around it. It never throws: a blank page is worse than an honest shell.\n */\nexport async function bootAuth(): Promise<BootResult> {\n if (!signInAvailable) return { kind: 'app' }\n\n const action = decideBoot({\n callback: callbackParams(),\n hasValidToken: currentToken() !== null,\n guardAttemptedAt: guardAttemptedAt(),\n enteredThisPageLoad: false,\n now: Date.now(),\n })\n\n switch (action.do) {\n case 'enter':\n startRenewal()\n return { kind: 'app' }\n case 'complete_callback':\n try {\n await completeSignIn()\n clearQuery()\n startRenewal()\n return { kind: 'app' }\n } catch (cause) {\n clearQuery()\n stashShellReason('exchange_failed')\n return shell('exchange_failed', cause)\n }\n case 'probe_silently':\n try {\n await beginSignIn({ silent: true })\n return { kind: 'leaving' }\n } catch {\n return shell('network')\n }\n case 'prompt_passkey':\n return { kind: 'shell', state: { phase: 'authenticating', reason: takeShellReason() ?? action.reason } }\n case 'fail':\n return shell(action.reason)\n }\n}\n\nfunction shell(reason: ShellReason, cause?: unknown): BootResult {\n if (cause) console.warn('[auth] entry failed:', cause)\n return { kind: 'shell', state: { phase: 'failed', reason } }\n}\n\nexport async function enterFromShell(): Promise<void> {\n await beginSignIn()\n}\n`,\n 'src/auth/AuthShell.tsx': `import { CONTINUE_LABEL, isRecoverable, RETRY_LABEL, SHELL_COPY, type ShellState } from '@estiva-app/identity'\nimport { Button } from '@estiva-app/ui'\nimport { APP_TITLE } from '../config'\n\nexport interface AuthShellProps {\n state: ShellState\n onContinue: () => void\n}\n\n/**\n * What shows while nobody is signed in: the app's name, one line and at most one\n * button. The words are \\`@estiva-app/identity\\`'s, so every app says the same.\n */\nexport function AuthShell({ state, onContinue }: AuthShellProps) {\n const waiting = state.phase === 'checking' || state.phase === 'entering' || state.phase === 'ready'\n return (\n <div className=\"fixed inset-0 z-50 flex flex-col items-center justify-center gap-4 bg-bg-surface px-6 text-center\">\n <h1 className=\"text-h3 text-text-primary\">{APP_TITLE}</h1>\n {!waiting && (\n <>\n <p className=\"max-w-prose text-body-2 text-text-secondary\">{SHELL_COPY[state.reason]}</p>\n {isRecoverable(state.reason) && <Button onClick={onContinue}>{state.phase === 'authenticating' ? CONTINUE_LABEL : RETRY_LABEL}</Button>}\n </>\n )}\n </div>\n )\n}\n`,\n 'src/relay/client.ts': `import { signViaEstivaId } from '@estiva-app/identity'\nimport { browserOnlineSource, createLiveClientHolder, type LiveClient } from '@estiva-app/platform'\nimport { currentToken } from '../auth/estivaId'\nimport { ID_CONFIG, RELAY_URL } from '../config'\n\n/**\n * The event kinds this app reads. They are the app's own decision, and the one\n * blank a new app fills in: Ship reads seven (projects, issues, changes,\n * messages, comments, deletions and edits), Peek its own. Until there are kinds\n * here the app is connected and reads nothing, which is a correct state.\n */\nexport const KINDS: number[] = []\n\n/**\n * The tab's one relay client. \\`@estiva-app/platform\\` hands out a holder and\n * keeps nothing, so this line is where \"one connection per tab\" lives. The relay\n * signs a connection in once and caps subscriptions per connection, so a\n * connection per component is wrong, not only wasteful; React's StrictMode,\n * which mounts everything twice, is what would show it.\n *\n * Peek and Ship hold theirs the same way. Everything the client needs is handed\n * in and nothing here reads the app's own data, so it could move into\n * \\`@estiva-app/platform\\` as it is, if that is ever worth doing.\n */\nconst holder = createLiveClientHolder()\n\n/**\n * The client, or \\`null\\` when this build cannot have one: no relay is set, or\n * there is no sign-in. The relay refuses anything before sign-in, so a socket\n * without it would only connect, be refused and retry.\n */\nexport function relayClient(): LiveClient | null {\n const base = ID_CONFIG?.base\n if (!RELAY_URL || !base) return null\n return holder.get({\n relayUrl: RELAY_URL,\n // Read on every connect, never kept: a token kept from the first connect\n // outlives a silent renewal as a dead one, and the next reconnect fails.\n getCredential: () => {\n const token = currentToken()\n return token ? { accessToken: token.accessToken, pubkey: token.pubkey } : null\n },\n // \\`expectedPubkey\\` is a check, not a request. Estiva ID signs as the token's\n // owner whatever it is handed, so a mismatch comes back as a valid event\n // signed by somebody else. Always pass it.\n sign: (unsigned, token, expectedPubkey) => signViaEstivaId(unsigned, { base, token, expectedPubkey }),\n online: typeof window === 'undefined' ? undefined : browserOnlineSource(window),\n // Names this app in the relay's own logs.\n subscriptionPrefix: '${name}-',\n log: (message, detail) => console.debug('[relay]', message, detail ?? ''),\n })\n}\n`,\n 'src/relay/useRelayState.ts': `import type { RelayState } from '@estiva-app/protocol'\nimport { useSyncExternalStore } from 'react'\nimport { relayClient } from './client'\n\nconst subscribe = (onChange: () => void): (() => void) => relayClient()?.onState(onChange) ?? (() => {})\nconst snapshot = (): RelayState | 'off' => relayClient()?.state() ?? 'off'\n\n/** The connection's state, for a page to show: \\`'off'\\` when this build has no client. */\nexport function useRelayState(): RelayState | 'off' {\n return useSyncExternalStore(subscribe, snapshot, () => 'off')\n}\n`,\n 'src/relay/client.test.ts': `import { afterEach, describe, expect, it, vi } from 'vitest'\n\n/**\n * The tab's one relay connection. A fake socket stands in for the relay, so this\n * runs with no network, and counts the sockets opened.\n */\nclass FakeSocket {\n static opened = 0\n onopen: (() => void) | null = null\n onmessage: ((event: { data: unknown }) => void) | null = null\n onclose: (() => void) | null = null\n onerror: (() => void) | null = null\n constructor() {\n FakeSocket.opened += 1\n }\n send() {}\n close() {}\n}\n\nafterEach(() => {\n vi.unstubAllEnvs()\n vi.unstubAllGlobals()\n vi.resetModules()\n FakeSocket.opened = 0\n})\n\ndescribe('the relay client', () => {\n it('opens no connection when no relay is set', async () => {\n vi.stubEnv('VITE_RELAY_URL', '')\n vi.stubEnv('VITE_ESTIVA_ID_ORIGIN', 'http://localhost:8787')\n vi.stubEnv('VITE_ESTIVA_ID_CLIENT_ID', '${name}')\n vi.stubGlobal('WebSocket', FakeSocket)\n const { relayClient } = await import('./client')\n expect(relayClient()).toBeNull()\n expect(FakeSocket.opened).toBe(0)\n })\n\n it('opens no connection with a relay and no sign-in', async () => {\n vi.stubEnv('VITE_RELAY_URL', 'http://localhost:3000')\n vi.stubEnv('VITE_ESTIVA_ID_ORIGIN', '')\n vi.stubGlobal('WebSocket', FakeSocket)\n const { relayClient } = await import('./client')\n expect(relayClient()).toBeNull()\n expect(FakeSocket.opened).toBe(0)\n })\n\n it('holds one connection per tab, however often it is asked', async () => {\n vi.stubEnv('VITE_RELAY_URL', 'http://localhost:3000')\n vi.stubEnv('VITE_ESTIVA_ID_ORIGIN', 'http://localhost:8787')\n vi.stubEnv('VITE_ESTIVA_ID_CLIENT_ID', '${name}')\n vi.stubGlobal('WebSocket', FakeSocket)\n const { relayClient } = await import('./client')\n const first = relayClient()\n expect(first).not.toBeNull()\n expect(relayClient()).toBe(first)\n expect(FakeSocket.opened).toBe(1)\n first?.close()\n })\n})\n`,\n 'src/main.tsx': `import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport './index.css'\nimport { AuthShell } from './auth/AuthShell'\nimport { bootAuth, enterFromShell } from './auth/boot'\n\n// Settle who this load belongs to first, then render: the app itself is imported\n// only once sign-in is settled, so nothing in it evaluates as the wrong person.\nvoid (async () => {\n const root = createRoot(document.getElementById('root')!)\n const boot = await bootAuth()\n if (boot.kind === 'leaving') return\n if (boot.kind === 'shell') {\n root.render(\n <StrictMode>\n <AuthShell state={boot.state} onContinue={() => void enterFromShell()} />\n </StrictMode>,\n )\n return\n }\n const { App } = await import('./App')\n root.render(\n <StrictMode>\n <App />\n </StrictMode>,\n )\n})()\n`,\n 'src/App.tsx': `import { AppShell, IdentityMenu, NavItem, Sidebar, type Identity } from '@estiva-app/ui'\nimport { IconHome } from '@tabler/icons-react'\nimport { useEffect, useState } from 'react'\nimport { beginSignOut, currentToken, whoAmI } from './auth/estivaId'\nimport { APP_TITLE, ID_CONFIG, RELAY_URL } from './config'\nimport { HomePage } from './pages/HomePage'\nimport { useRelayState } from './relay/useRelayState'\n\n/** The frame: the package's AppShell with a sidebar, and the one page. */\nexport function App() {\n const signedIn = currentToken() !== null\n const [me, setMe] = useState<Identity>({})\n const relayState = useRelayState()\n\n useEffect(() => {\n if (!signedIn) return\n let live = true\n void whoAmI().then((who) => {\n if (live && who) setMe(who)\n })\n return () => {\n live = false\n }\n }, [signedIn])\n\n return (\n <AppShell\n logo={APP_TITLE}\n identity={<IdentityMenu me={me} signedIn={signedIn} idBase={ID_CONFIG?.base} onSignOut={signedIn ? beginSignOut : undefined} />}\n nav={\n <Sidebar>\n <NavItem href=\"/\" label=\"Home\" icon={<IconHome size={16} stroke={1.5} />} active />\n </Sidebar>\n }\n >\n <HomePage relay={RELAY_URL} state={relayState} name={me.name} />\n </AppShell>\n )\n}\n`,\n 'src/pages/HomePage.tsx': `import type { RelayState } from '@estiva-app/protocol'\nimport { EmptyState } from '@estiva-app/ui'\nimport { relayLabel } from '../config'\n\nexport interface HomePageProps {\n /** The relay this build is set to, or \\`null\\` when the app runs alone. */\n relay: string | null\n /** The connection's state: \\`'off'\\` when this build has no client. */\n state: RelayState | 'off'\n /** The signed-in person's name, once Estiva ID has said it. */\n name?: string\n}\n\nconst SAYS: Record<RelayState, (host: string, name?: string) => string> = {\n connecting: (host) => \\`Connecting to \\${host}\u2026\\`,\n authenticating: (host) => \\`Signing in to \\${host}\u2026\\`,\n live: (host, name) => (name ? \\`Connected to \\${host} as \\${name}.\\` : \\`Connected to \\${host}.\\`),\n reconnecting: (host) => \\`Reconnecting to \\${host}\u2026\\`,\n failed: (host) => \\`Could not connect to \\${host}.\\`,\n}\n\n/**\n * The first page. What it becomes is this app's own work; until then it says\n * whether the app is connected to the relay, and as whom.\n *\n * The empty state goes straight into the frame's \\`main\\`, with no box around it:\n * \\`main\\` is a flex column, so the empty state takes the room left and centres\n * in it both ways, as its own page says. A box around it would place it\n * instead, and no gate reads a box. The connection is a quiet caption after it,\n * at the foot of the page.\n */\nexport function HomePage({ relay, state, name }: HomePageProps) {\n const line = !relay\n ? 'Running alone: no relay is set. Set VITE_RELAY_URL in .env.local to connect.'\n : state === 'off'\n ? \\`Not connected: \\${relayLabel(relay)} needs sign-in, and this build has none.\\`\n : SAYS[state](relayLabel(relay), name)\n return (\n <>\n <EmptyState message=\"Nothing here yet.\" />\n <p className=\"px-6 pb-6 text-center text-caption text-text-muted\">{line}</p>\n </>\n )\n}\n`,\n 'src/pages/HomePage.stories.tsx': `import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { HomePage } from './HomePage'\n\nconst meta = {\n title: 'Pages/Home',\n component: HomePage,\n parameters: { layout: 'fullscreen' },\n // Stands in for the frame's main: a flex column the height of the screen,\n // which is what the page is drawn into in the app.\n decorators: [\n (Story) => (\n <div className=\"flex h-screen flex-col\">\n <Story />\n </div>\n ),\n ],\n} satisfies Meta<typeof HomePage>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n/** No relay set: the app runs alone, the way it starts. */\nexport const RunningAlone: Story = { args: { relay: null, state: 'off' } }\n\n/** A relay set, before it has signed the connection in. */\nexport const Connecting: Story = { args: { relay: 'http://localhost:3000', state: 'connecting' } }\n\n/** Connected, as the person who signed in. */\nexport const Connected: Story = { args: { relay: 'http://localhost:3000', state: 'live', name: 'Alex Kim' } }\n`,\n 'src/App.test.tsx': `import { render, screen } from '@testing-library/react'\nimport { describe, expect, it } from 'vitest'\nimport { App } from './App'\nimport { APP_TITLE } from './config'\n\ndescribe('${title}', () => {\n it('opens in its frame, anonymous in a build with no sign-in', () => {\n render(<App />)\n expect(screen.getAllByText(APP_TITLE).length).toBeGreaterThan(0)\n expect(screen.getByText('Nothing here yet.')).toBeTruthy()\n expect(screen.getByText(/^Running alone/)).toBeTruthy()\n expect(screen.getByRole('link', { name: 'Home' })).toBeTruthy()\n })\n})\n`,\n\n '.storybook/main.ts': `import type { StorybookConfig } from '@storybook/react-vite'\n\nconst config: StorybookConfig = {\n framework: '@storybook/react-vite',\n stories: ['../src/**/*.stories.@(ts|tsx)'],\n addons: ['@storybook/addon-docs'],\n}\n\nexport default config\n`,\n '.storybook/preview.tsx': `import type { Preview } from '@storybook/react-vite'\nimport '../src/index.css'\n\n// The app's one theme, selected the way the app selects it: on <html>.\ndocument.documentElement.dataset.theme = ${JSON.stringify(theme)}\n\nconst preview: Preview = {\n // Every story file gets a Docs page, as in Peek and Ship: the catalogue links each\n // part to it, and \\`npm run registry:check\\` in CI's job \\`gate\\` fails a link that\n // leads nowhere.\n tags: ['autodocs'],\n parameters: { layout: 'centered' },\n}\n\nexport default preview\n`,\n\n 'README.md': `# ${title}\n\nAn Estiva app, made with \\`create-estiva-app\\` from \\`@estiva-app/ui\\`. It starts\nwith the package's sidebar frame, one theme (\\`${theme}\\`), sign-in with Estiva ID,\nand every UI Guardrails gate on, at zero.\n\n## Run it\n\n\\`\\`\\`sh\nnpm install\nnpm run dev\n\\`\\`\\`\n\nWith no settings it runs **anonymous**: no sign-in is offered, and nothing reaches\nthe real Estiva ID. To sign in, copy \\`.env.example\\` to \\`.env.local\\` and fill it in.\nThe app must first be registered with that Estiva ID as its own app.\n\n## The relay\n\n${title} is connected to the relay, the workspace, from its first commit, as\nwhoever signed in. \\`src/relay/client.ts\\` holds the tab's one connection: ask it\nfor \\`relayClient()\\`, and never open a socket of your own. The home page shows the\nconnection's state.\n\nWith \\`VITE_RELAY_URL\\` empty it runs alone and opens no connection. The relay\nalso needs sign-in, so with a relay and no sign-in there is no connection either.\n\nThree packages come with it: \\`@estiva-app/protocol\\` (the wire: events, ids,\nsigning, the relay clients), \\`@estiva-app/platform\\` (the one connection a tab\nholds) and \\`@estiva-app/interop\\` (showing another app's objects, from the\nmanifest that app publishes).\n\n## What you fill in\n\n1. \\`.env.local\\`: Estiva ID and the relay (see \\`.env.example\\`).\n2. \\`KINDS\\` in \\`src/relay/client.ts\\`: the event kinds ${title} reads. Until then\n it is connected and reads nothing.\n3. The product: its pages, and the fold that turns what arrives on the\n connection into the app's state.\n\nBefore ${title} can sign in on the real Estiva ID it has to be registered there\nas its own app, with:\n\n- its exact redirect address\n- every event kind it will sign, **including 22242**, the relay's sign-in handshake\n- the relay's address, allowed for that handshake. Estiva ID signs a handshake\n only for a relay both the app and the deployment allow.\n\nAnything left out fails at the very last step: every screen looks right, and\nnothing arrives.\n\n## The checks\n\n| command | what |\n|---|---|\n| \\`npm run typecheck\\` | TypeScript, the app and its settings files |\n| \\`npm run lint\\` | everything: TypeScript's and React's rules, the token contract, the gate |\n| \\`npm run lint:rules\\` | the gate alone, and \\`.gates-count.json\\` |\n| \\`npm test\\` | the tests |\n| \\`npm run build\\` | the build |\n| \\`npm run gates:status\\` | which gates are on, read from the code |\n| \\`npm run ui:find <words>\\` | what the package and this app already have for it |\n| \\`npm run registry:check\\` | every part says what it is for |\n| \\`npx estiva-gates ci\\` | every step of CI's job \\`gate\\`: the gate, its count committed, the tokens, the catalogue, the hook |\n| \\`npm run storybook\\` | the stories |\n\nThe gates are the package's, imported rather than copied, so a rule written later\narrives with an ordinary version bump. A Claude session started in this folder is\nstopped before it writes code the gate refuses (\\`.claude/settings.json\\`).\n\n## Once it is on GitHub\n\nRequire the check \\`gate\\` before anything merges into \\`main\\`: a ruleset on \\`main\\`\n(Settings \u2192 Rules \u2192 Rulesets) with \"Require status checks to pass\", the check\n\\`gate\\`, and no one allowed to bypass it. Until then \\`npm run gates:status\\` shows\nUIG-6 as not done.\n`,\n 'CLAUDE.md': `# ${title}, for Claude Code\n\n${title} is an Estiva app. Its parts, tokens and gates come from \\`@estiva-app/ui\\`.\n\n**Use the package's parts, never a raw element or a hand-built look.** The gate\nrefuses code that reaches past the package, and each refusal names what to use\ninstead. What it refuses is the package's to say, and grows with it: the\n\\`estiva-ui\\` skill lists it. It runs before you write\n(the hook in \\`.claude/settings.json\\`, for a session started in this folder), in\n\\`npm run lint:rules\\` and in CI. A session started elsewhere: run\n\\`npm run lint:rules\\` after changing \\`src/\\`, and fix what it reports. Keep something\nonly with its reason on the line above, \\`// @estiva-escape(<rule>): <reason>\\`, never with\n\\`eslint-disable\\`.\n\n**Tokens only.** Colours, type, corners and shadows come from the package's preset.\n\n**One relay connection per tab.** \\`relayClient()\\` in \\`src/relay/client.ts\\` is the\nconnection. Never open a socket and never make a second holder: the relay signs a\nconnection in once, and caps subscriptions per connection.\n\n**Look before you build.** \\`npm run ui:find <what it does>\\` searches the package's\nparts and this app's own. Use what it finds.\n\n**Every part says what it is for.** A new part gets a one-line \\`/** \u2026 */\\` comment\ndirectly above it. \\`npm run registry:check\\` and CI's job \\`gate\\` refuse a part without one.\n\n**The count starts at zero and stays there** (\\`.gates-count.json\\`, \\`docs/GATES-DEBT.md\\`).\n\nWhat each gate is and how it is wired: the package's README,\n\\`node_modules/@estiva-app/ui/README.md\\`.\n`,\n }\n}\n\n/** The versions of what this package does not use itself, from the npm registry. */\nexport function askNpm(names: string[]): Record<string, string> {\n const npm = process.env.npm_execpath\n const out: Record<string, string> = {}\n for (const name of names) {\n const args = ['view', name, 'version']\n const version = (npm ? execFileSync(process.execPath, [npm, ...args]) : execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { shell: process.platform === 'win32' }))\n .toString()\n .trim()\n if (!/^\\d+\\.\\d+\\.\\d+/.test(version)) throw new Error(`npm did not say which version of ${name} is current: \"${version}\"`)\n out[name] = `^${version}`\n }\n return out\n}\n\n/** Write the app. Refuses a folder that already exists. Returns the folder. */\nexport function createApp(options: CreateAppOptions): string {\n const dir = resolve(options.parent ?? process.cwd(), options.name)\n if (existsSync(dir)) throw new Error(`${dir} already exists: create-estiva-app never writes into a folder that is there`)\n const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')) as PackageJson\n const missing = ASKED_OF_NPM.filter((n) => !pkg.devDependencies[n] && !options.versions?.[n])\n const versions = { ...(missing.length ? askNpm(missing) : {}), ...options.versions }\n const files = appFiles({ ...options, versions })\n for (const [rel, text] of Object.entries(files)) {\n const path = join(dir, rel)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, text)\n }\n return dir\n}\n", "/**\n * The names of the app rules, with nothing imported.\n *\n * `create-estiva-app` needs these names to write a new app's count at zero, and\n * nothing else of the plugin. It runs from `npx` in an empty folder, where only\n * this package's own dependencies are installed, so it must never load the\n * rules themselves: since `no-copied-look` (UIG-25, 0.34.0) they import\n * TypeScript, and that import stopped the command before it wrote a file\n * (UIG-10, reopened 27 September). `index.ts` holds its rule table to exactly\n * these names, so the two cannot drift.\n */\n\n/** The name the configs register the plugin under, so every rule id is `estiva/<rule>`. */\nexport const PLUGIN_KEY = 'estiva'\n\nexport const APP_RULE_NAMES = [\n 'no-raw-element',\n 'no-rebuilt-behaviour',\n 'no-restyled-part',\n 'no-handmade-header',\n 'no-handmade-empty-state',\n 'no-native-title',\n 'no-copied-look',\n] as const\n\nexport type AppRuleName = (typeof APP_RULE_NAMES)[number]\n\n/** The ids `recommended` and `strict` carry \u2014 what an app's gate runs and counts. */\nexport const APP_RULE_IDS = APP_RULE_NAMES.map((name) => `${PLUGIN_KEY}/${name}`)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `create-estiva-app <name>` \u2014 a new Estiva app that runs, with every gate on\n * (UIG-10).\n *\n * npx -p @estiva-app/ui create-estiva-app leaf [--title Leaf] [--theme light]\n *\n * The command is `create-app-cli.ts`; this file is what it runs, and what a test reads.\n *\n * **It reads only this package** (Katerina's ruling of 17 September, docs/GATES.md\n * \u00A723). Never Peek, never Ship: they are private, and reading them would carry\n * their history into every new app. What a new app needs to run a gate is this\n * package's own \u2014 the rules, the token and gate configs, the hook, the count,\n * gates:status and the checks every app runs \u2014 so the app gets short files that\n * import them, and a gate added later arrives with a version bump.\n *\n * What only a new app has is written here: its first page in the sidebar frame\n * (ruled the same day), its one theme, sign-in the way Ship signs in, its one\n * relay connection the way Peek and Ship hold theirs, its tests, its stories,\n * the CI workflow with the job `gate`, and its README and CLAUDE.md.\n *\n * The relay (UIG-10, reopened 18 September): a made app gets `protocol`,\n * `platform` and `interop` and is connected from its first commit, or runs alone\n * when no relay is set. The wiring is the app's own file, using `platform` as it\n * is \u2014 not a helper in `platform` \u2014 because a package takes code a real app has\n * already run (ADR 0002 \u00A710), and Peek and Ship each hold theirs the same way.\n *\n * Tool versions are this package's own (its devDependencies build and test the\n * same tools). The ones it does not use itself ({@link ASKED_OF_NPM}) are asked of\n * the npm registry when the app is made. The lockfile is the app's first\n * `npm install`; make it on Linux.\n */\nimport { execFileSync } from 'node:child_process'\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { APP_RULE_IDS } from '../eslint/rule-ids'\nimport { loaderText, SEARCH_RULES } from '../registry/skill'\n\ninterface PackageJson {\n version: string\n devDependencies: Record<string, string>\n}\n\nexport interface CreateAppOptions {\n /** The folder and npm name: lowercase letters, digits and dashes. */\n name: string\n /** What the app is called on screen. */\n title?: string\n /** One of this package's themes. */\n theme?: string\n /** The folder the app's folder is made in. */\n parent?: string\n /** The `@estiva-app/ui` dependency, when not this package's own version (a tarball, for a proof). */\n ui?: string\n /** Versions of the packages this package does not itself use; asked of npm when absent. */\n versions?: Record<string, string>\n}\n\nconst here = dirname(fileURLToPath(import.meta.url))\nconst packageRoot = resolve(here, '..', '..')\n\n/** What a made app depends on that this package does not use itself: asked of npm when the app is made. */\nexport const ASKED_OF_NPM = ['@estiva-app/identity', '@estiva-app/interop', '@estiva-app/platform', '@estiva-app/protocol', 'eslint-plugin-react-hooks']\n\n/** The package's themes, read from its tokens.css: `light` is the block with no attribute. */\nexport function themes(): string[] {\n const css = readFileSync(join(packageRoot, 'tokens.css'), 'utf8')\n return ['light', ...new Set([...css.matchAll(/data-theme='([\\w-]+)'/g)].map((m) => m[1]))]\n}\n\n/** Every file a new app starts with, by path. Nothing is written. */\nexport function appFiles({ name, title = name, theme = 'light', ui, versions = {} }: CreateAppOptions): Record<string, string> {\n if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`\"${name}\" is not a name an app can have: lowercase letters, digits and dashes, starting with a letter`)\n const known = themes()\n if (!known.includes(theme)) throw new Error(`\"${theme}\" is not one of the package's themes: ${known.join(', ')}`)\n\n const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')) as PackageJson\n const own = (dep: string) => {\n const range = pkg.devDependencies[dep] ?? versions[dep]\n if (!range) throw new Error(`no version for ${dep}`)\n return range\n }\n const deps = (names: string[]) => Object.fromEntries(names.map((n) => [n, own(n)]))\n\n const packageJson = {\n name,\n private: true,\n version: '0.0.0',\n type: 'module',\n scripts: {\n dev: 'vite',\n build: 'tsc -b && vite build',\n preview: 'vite preview',\n typecheck: 'tsc -b',\n lint: 'eslint .',\n 'lint:tokens': 'eslint --config eslint.tokens.config.js .',\n 'lint:rules': 'eslint --config eslint.gates.config.js .',\n 'postlint:rules': `estiva-gates count --repo ${name}`,\n 'gates:status': 'estiva-gates status',\n 'ui:find': 'estiva-ui find',\n registry: 'estiva-ui build',\n 'registry:check': 'estiva-ui check',\n test: 'vitest run',\n storybook: 'storybook dev -p 6006',\n 'build-storybook': 'storybook build',\n },\n dependencies: {\n '@estiva-app/identity': own('@estiva-app/identity'),\n '@estiva-app/interop': own('@estiva-app/interop'),\n '@estiva-app/platform': own('@estiva-app/platform'),\n '@estiva-app/protocol': own('@estiva-app/protocol'),\n '@estiva-app/ui': ui ?? `^${pkg.version}`,\n ...deps(['@tabler/icons-react', 'react', 'react-dom']),\n },\n devDependencies: deps([\n '@eslint/js', '@storybook/addon-docs', '@storybook/react-vite', '@testing-library/dom', '@testing-library/react',\n '@types/node', '@types/react', '@types/react-dom', '@vitejs/plugin-react', 'autoprefixer', 'eslint',\n 'eslint-plugin-better-tailwindcss', 'eslint-plugin-react-hooks', 'globals', 'jsdom', 'postcss', 'storybook',\n 'tailwindcss', 'typescript', 'typescript-eslint', 'vite', 'vitest',\n ]),\n }\n\n const count = { schemaVersion: 1, repo: name, generatedAt: new Date().toISOString(), rules: Object.fromEntries(APP_RULE_IDS.map((id) => [id, { errors: 0, warnings: 0, escapes: 0 }])) }\n const themeAttr = ` data-theme=\"${theme}\"`\n const tsBase = {\n target: 'ES2023',\n module: 'ESNext',\n skipLibCheck: true,\n moduleResolution: 'bundler',\n allowImportingTsExtensions: true,\n verbatimModuleSyntax: true,\n moduleDetection: 'force',\n noEmit: true,\n strict: true,\n noUnusedLocals: false,\n noUnusedParameters: false,\n erasableSyntaxOnly: true,\n noFallthroughCasesInSwitch: true,\n noUncheckedSideEffectImports: true,\n }\n const json = (value: unknown) => `${JSON.stringify(value, null, 2)}\\n`\n\n return {\n 'package.json': json(packageJson),\n\n // registry.json: the app's catalogue, written only when `npm run registry` is asked\n // to. It is built fresh from the code every time it is read, so it is never kept.\n '.gitignore': ['node_modules', 'dist', 'storybook-static', '*.local', '*.log', '*.tsbuildinfo', '.DS_Store', 'registry.json', ''].join('\\n'),\n\n '.env.example': `# Estiva ID, for signing in. Copy this file to .env.local and fill both in.\n#\n# Left empty, the app offers no sign-in at all and runs anonymous: it never\n# reaches the real Estiva ID by accident.\n#\n# The app must be registered with that Estiva ID first, as its own app, with\n# this redirect address: the origin the app runs on, and a slash\n# (http://localhost:5173/). The real Estiva ID registers a new app on the\n# server; a local one (http://localhost:8787) in its database.\nVITE_ESTIVA_ID_ORIGIN=\nVITE_ESTIVA_ID_CLIENT_ID=${name}\n\n# The relay: the workspace this app reads and writes.\n#\n# Left empty, the app runs alone. It opens no connection, and its home page says\n# so. It also needs sign-in above: the relay refuses anything before sign-in, so\n# with a relay and no sign-in there is still no connection. And Estiva ID must\n# allow this app to sign in to that relay (the README says how).\n#\n# A relay running on this machine is http://localhost:3000.\nVITE_RELAY_URL=\n`,\n\n 'index.html': `<!doctype html>\n<html lang=\"en\"${themeAttr}>\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${title}</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"/src/main.tsx\"></script>\n </body>\n</html>\n`,\n\n 'tsconfig.json': json({ files: [], references: [{ path: './tsconfig.app.json' }, { path: './tsconfig.node.json' }] }),\n 'tsconfig.app.json': json({\n compilerOptions: { tsBuildInfoFile: './node_modules/.tmp/tsconfig.app.tsbuildinfo', ...tsBase, useDefineForClassFields: true, lib: ['ES2023', 'DOM', 'DOM.Iterable'], types: ['vite/client'], jsx: 'react-jsx', paths: { '@/*': ['./src/*'] } },\n include: ['src', '.storybook'],\n }),\n 'tsconfig.node.json': json({\n compilerOptions: { tsBuildInfoFile: './node_modules/.tmp/tsconfig.node.tsbuildinfo', ...tsBase, lib: ['ES2023'], types: ['node'] },\n include: ['vite.config.ts'],\n }),\n\n 'vite.config.ts': `import { fileURLToPath } from 'node:url'\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vitest/config'\n\n// \\`react\\` is deduped because @estiva-app/ui declares it a peer: two copies in one\n// tree is the \"invalid hook call\" crash, and it appears at runtime.\nexport default defineConfig({\n plugins: [react()],\n resolve: {\n alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },\n dedupe: ['react', 'react-dom'],\n },\n test: {\n environment: 'jsdom',\n },\n})\n`,\n\n 'tailwind.config.js': `import estiva, { estivaContent } from '@estiva-app/ui/tailwind-preset'\n\n/**\n * The package's preset, and nothing of the app's own: a token is added to the\n * package, in every theme, never here.\n *\n * \\`estivaContent\\` is the package's own files. Tailwind does not merge \\`content\\`\n * from a preset, so without it every class used only by a package part is\n * purged, and the app builds clean while rendering at the wrong size.\n *\n * @type {import('tailwindcss').Config}\n */\nexport default {\n presets: [estiva],\n content: [...estivaContent, './index.html', './src/**/*.{ts,tsx}', './.storybook/**/*.{ts,tsx}'],\n}\n`,\n 'postcss.config.js': `export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n`,\n\n 'eslint.config.js': `import js from '@eslint/js'\nimport { gateLint, TOKEN_LINT_IGNORES, tokenLint, tokenValues } from '@estiva-app/ui/gates'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport { defineConfig, globalIgnores } from 'eslint/config'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\n/**\n * Everything, in one lint (\\`npm run lint\\`, a CI step): TypeScript's and React's\n * recommended rules, the token contract and the UI Guardrails' rules. A new app\n * has no backlog, so all of it is a gate from the first commit.\n *\n * The token contract and the gate are the package's (\\`@estiva-app/ui/gates\\`),\n * imported, never copied: a rule written later arrives with a version bump.\n */\nexport default defineConfig([\n globalIgnores(TOKEN_LINT_IGNORES),\n {\n files: ['**/*.{ts,tsx}'],\n extends: [js.configs.recommended, tseslint.configs.recommended, reactHooks.configs.flat.recommended],\n languageOptions: { globals: globals.browser },\n },\n tokenLint(),\n tokenValues(),\n gateLint(),\n])\n`,\n 'eslint.tokens.config.js': `import { tokenConfig } from '@estiva-app/ui/gates'\nimport reactHooks from 'eslint-plugin-react-hooks'\n\n// The token contract on its own (\\`npm run lint:tokens\\`), the package's piece.\n// React's hooks plugin is named so its directives do not break it; TypeScript's\n// the package names itself.\nexport default tokenConfig({ quiet: { 'react-hooks': reactHooks } })\n`,\n 'eslint.gates.config.js': `import { gateConfig } from '@estiva-app/ui/gates'\nimport reactHooks from 'eslint-plugin-react-hooks'\n\n/**\n * The UI Guardrails' rules on their own \u2014 \\`npm run lint:rules\\`, CI's job \\`gate\\`\n * (GitHub requires it on main: never rename it), and what the editor hook in\n * \\`.claude/settings.json\\` lints a proposed write with. The package's gate,\n * imported. React's hooks plugin is named so its directives do not break it;\n * TypeScript's the package names itself.\n *\n * A place that keeps something the gate refuses says why, on the line above:\n * \\`// @estiva-escape(<rule>): <reason>\\`, naming the rule it keeps off. Never\n * \\`eslint-disable\\`: the count refuses it.\n */\nexport default gateConfig({ quiet: { 'react-hooks': reactHooks } })\n`,\n\n '.claude/settings.json': json({\n hooks: {\n PreToolUse: [{ matcher: 'Edit|Write', hooks: [{ type: 'command', command: 'node \"$CLAUDE_PROJECT_DIR/node_modules/@estiva-app/ui/dist/gates/cli.js\" hook' }] }],\n },\n permissions: { allow: SEARCH_RULES },\n }),\n\n // The Claude skill (UIG-20): its trigger, and a line reading the installed package's text in.\n '.claude/skills/estiva-ui/SKILL.md': loaderText(resolve('/app'), resolve('/app/node_modules/@estiva-app/ui')),\n\n 'scripts/gates-checks.mjs': `import { appChecks } from '@estiva-app/ui/gates'\n\n/**\n * What gates:status checks in ${name}: the gate checks every app runs, from the\n * package (\\`appChecks\\`), and nothing of its own yet. A check about this app's\n * own code goes beside them.\n */\nexport default function define(h) {\n return { repo: '${name}', tickets: appChecks(h, { page: 'src/pages/HomePage.tsx' }) }\n}\n`,\n '.gates-count.json': json(count),\n 'docs/GATES-DEBT.md': `# What ${title} owes the gates\n\nNothing. ${title} was made with every gate on, at zero.\n\nIt should stay that way. A place that keeps something the gate refuses says why on\nthe line above it, \\`// @estiva-escape(<rule>): <reason>\\`. This page does not list those:\n\\`npm run gates:status -- --escapes\\` does, from the code, with each one's age, and\n\\`npm run gates:status\\` shows the ones older than 30 days, to keep with a new reason or fix.\n\n## Folders the gate does not read\n\nNone. A folder the gate skips is named here, with why; CI's job \\`gate\\` fails on one\nthat is not.\n`,\n\n '.github/workflows/deploy.yml': `name: deploy\n\n# Every pull request and every push to main runs the checks. There is no deploy\n# job yet: a new app has nowhere to go until it has a home. When it does, the job\n# goes here, and it needs \\`check\\` and \\`gate\\` first.\n\non:\n push:\n branches: [main]\n pull_request:\n\nconcurrency:\n group: deploy-${name}-\\${{ github.ref }}\n cancel-in-progress: true\n\njobs:\n check:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 24\n cache: npm\n - run: npm ci\n - name: Typecheck the app\n run: npx tsc -b tsconfig.app.json\n - name: Typecheck the settings files\n run: npx tsc -b tsconfig.node.json\n - name: Lint \u2014 everything, the token contract included\n run: npm run lint\n - name: Test\n run: npm test\n - name: Build\n run: npm run build\n # A class only a package part uses reaches the stylesheet only if\n # tailwind.config.js spreads estivaContent. When it does not, the build still\n # succeeds and parts render at the wrong size, so this is where it is caught.\n - name: The package's classes survive Tailwind's purge\n run: |\n if ! grep -qr \"max-h-72\" dist/assets/*.css; then\n echo \"::error::a class used only by @estiva-app/ui is missing from the built CSS \u2014 check estivaContent is spread into tailwind.config.js\"\n exit 1\n fi\n\n # The UI Guardrails, the one check a merge requires: the package's gate rules\n # (each refusal names what to use instead), the token contract, and the\n # catalogue. \\`npm run lint:rules\\` also writes .gates-count.json and fails on\n # an eslint-disable that switches a rule off.\n #\n # A job of its own, because GitHub can require only a whole job, by its name:\n # the rule on main requires \\`gate\\`. Renaming it leaves every pull request\n # waiting for a check that never reports.\n gate:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 24\n cache: npm\n - run: npm ci\n # Every step of the gate, the same command Peek and Ship run: the gate\n # lint and \\`.gates-count.json\\` (committed, or this fails), the token\n # contract, the catalogue (every part says in one line what it is for),\n # and the wall's own settings: the editor hook in .claude/settings.json\n # refuses a raw button, and every folder the gate skips is named in\n # docs/GATES-DEBT.md.\n - name: The gate (estiva-gates ci)\n run: npx estiva-gates ci\n`,\n\n 'src/index.css': `@import '@estiva-app/ui/tokens.css';\n@import '@estiva-app/ui/base.css';\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nhtml,\nbody,\n#root {\n height: 100%;\n}\n`,\n 'src/vite-env.d.ts': `/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n readonly VITE_ESTIVA_ID_ORIGIN?: string\n readonly VITE_ESTIVA_ID_CLIENT_ID?: string\n readonly VITE_RELAY_URL?: string\n}\n`,\n 'src/config.ts': `/** What the app is called on screen. */\nexport const APP_TITLE = ${JSON.stringify(title)}\n\n/**\n * Estiva ID, or \\`null\\` when this build offers no sign-in at all. Empty is a real\n * mode, not a broken one: the app runs anonymous, and a local build can never\n * reach the real Estiva ID by accident. See .env.example.\n */\nexport const ID_CONFIG: { base: string; clientId: string } | null =\n import.meta.env.VITE_ESTIVA_ID_ORIGIN && import.meta.env.VITE_ESTIVA_ID_CLIENT_ID\n ? { base: import.meta.env.VITE_ESTIVA_ID_ORIGIN.replace(/\\\\/+$/, ''), clientId: import.meta.env.VITE_ESTIVA_ID_CLIENT_ID }\n : null\n\n/**\n * The relay, the workspace this app reads and writes, or \\`null\\` when this build\n * has none. Empty is a real mode too: the app runs alone and opens no connection,\n * and a local build can never reach the real relay by accident. See .env.example.\n */\nexport const RELAY_URL: string | null = import.meta.env.VITE_RELAY_URL?.trim().replace(/\\\\/+$/, '') || null\n\n/** The relay as a person would name it: host and port, no scheme. */\nexport function relayLabel(url: string): string {\n try {\n return new URL(url).host\n } catch {\n return url\n }\n}\n`,\n 'src/auth/estivaId.ts': `import { createEstivaId, type ShellReason, type StoredToken } from '@estiva-app/identity'\nimport { ID_CONFIG } from '../config'\n\n/**\n * Signing in with Estiva ID, the way Ship does: \\`@estiva-app/identity\\` holds the\n * flow, this file the app's choices.\n *\n * - The redirect is the origin and a slash, never the current path: Estiva ID\n * compares it exactly against the one address registered for the app.\n * - The session lives in localStorage, so it outlives a tab; a sign-in's\n * single-use credentials in sessionStorage, per tab.\n */\nconst client = ID_CONFIG\n ? createEstivaId({\n base: ID_CONFIG.base,\n clientId: ID_CONFIG.clientId,\n redirectUri: () => \\`\\${window.location.origin}/\\`,\n storage: () => (typeof localStorage === 'undefined' ? null : localStorage),\n pendingStore: () => (typeof sessionStorage === 'undefined' ? null : sessionStorage),\n keyPrefix: '${name}.estiva-id',\n navigate: (url) => window.location.assign(url),\n })\n : null\n\nexport const signInAvailable = client !== null\n\nexport function currentToken(): StoredToken | null {\n return client?.validToken(Date.now() + 60_000) ?? null\n}\n\nexport function startRenewal(): void {\n client?.scheduleRenewal()\n}\n\nexport async function beginSignIn(options: { silent?: boolean } = {}): Promise<void> {\n await client?.beginSignIn(\\`\\${window.location.pathname}\\${window.location.search}\\`, options)\n}\n\nexport function beginSignOut(): void {\n client?.beginSignOut()\n}\n\nexport async function completeSignIn(): Promise<{ token: StoredToken; returnTo: string }> {\n if (!client) throw new Error('this build offers no sign-in')\n return client.completeSignIn(window.location.search)\n}\n\nexport function stashShellReason(reason: ShellReason): void {\n client?.stashShellReason(reason)\n}\n\nexport function takeShellReason() {\n return client?.takeShellReason() ?? null\n}\n\nexport function callbackParams(): { code?: string; error?: string } | null {\n const params = new URLSearchParams(window.location.search)\n const code = params.get('code') ?? undefined\n const error = params.get('error') ?? undefined\n return code || error ? { code, error } : null\n}\n\nexport function clearQuery(): void {\n window.history.replaceState({}, '', window.location.pathname)\n}\n\nexport function guardAttemptedAt(): number | null {\n if (typeof sessionStorage === 'undefined') return null\n const raw = sessionStorage.getItem('estiva.authShell.silentAttempt')\n const at = raw ? Number(raw) : NaN\n return Number.isFinite(at) ? at : null\n}\n\n/**\n * Who Estiva ID says this is. Asked of its directory with the token, so the\n * answer also proves Estiva ID accepted the sign-in.\n */\nexport async function whoAmI(): Promise<{ name?: string; email?: string } | null> {\n const token = currentToken()\n if (!ID_CONFIG || !token) return null\n const response = await fetch(\\`\\${ID_CONFIG.base}/directory/\\${token.pubkey}\\`, { headers: { Authorization: \\`Bearer \\${token.accessToken}\\` } })\n if (!response.ok) return null\n const entry = (await response.json()) as { displayName?: string; email?: string }\n return { name: entry.displayName, email: entry.email }\n}\n`,\n 'src/auth/boot.ts': `import { decideBoot, type ShellReason, type ShellState } from '@estiva-app/identity'\nimport {\n beginSignIn,\n callbackParams,\n clearQuery,\n completeSignIn,\n currentToken,\n guardAttemptedAt,\n signInAvailable,\n startRenewal,\n stashShellReason,\n takeShellReason,\n} from './estivaId'\n\nexport type BootResult = { kind: 'app' } | { kind: 'shell'; state: ShellState } | { kind: 'leaving' }\n\n/**\n * Settle who this load belongs to, before the app renders. The decision is\n * \\`decideBoot\\` from \\`@estiva-app/identity\\`; this is the reading and acting\n * around it. It never throws: a blank page is worse than an honest shell.\n */\nexport async function bootAuth(): Promise<BootResult> {\n if (!signInAvailable) return { kind: 'app' }\n\n const action = decideBoot({\n callback: callbackParams(),\n hasValidToken: currentToken() !== null,\n guardAttemptedAt: guardAttemptedAt(),\n enteredThisPageLoad: false,\n now: Date.now(),\n })\n\n switch (action.do) {\n case 'enter':\n startRenewal()\n return { kind: 'app' }\n case 'complete_callback':\n try {\n await completeSignIn()\n clearQuery()\n startRenewal()\n return { kind: 'app' }\n } catch (cause) {\n clearQuery()\n stashShellReason('exchange_failed')\n return shell('exchange_failed', cause)\n }\n case 'probe_silently':\n try {\n await beginSignIn({ silent: true })\n return { kind: 'leaving' }\n } catch {\n return shell('network')\n }\n case 'prompt_passkey':\n return { kind: 'shell', state: { phase: 'authenticating', reason: takeShellReason() ?? action.reason } }\n case 'fail':\n return shell(action.reason)\n }\n}\n\nfunction shell(reason: ShellReason, cause?: unknown): BootResult {\n if (cause) console.warn('[auth] entry failed:', cause)\n return { kind: 'shell', state: { phase: 'failed', reason } }\n}\n\nexport async function enterFromShell(): Promise<void> {\n await beginSignIn()\n}\n`,\n 'src/auth/AuthShell.tsx': `import { CONTINUE_LABEL, isRecoverable, RETRY_LABEL, SHELL_COPY, type ShellState } from '@estiva-app/identity'\nimport { Button } from '@estiva-app/ui'\nimport { APP_TITLE } from '../config'\n\nexport interface AuthShellProps {\n state: ShellState\n onContinue: () => void\n}\n\n/**\n * What shows while nobody is signed in: the app's name, one line and at most one\n * button. The words are \\`@estiva-app/identity\\`'s, so every app says the same.\n */\nexport function AuthShell({ state, onContinue }: AuthShellProps) {\n const waiting = state.phase === 'checking' || state.phase === 'entering' || state.phase === 'ready'\n return (\n <div className=\"fixed inset-0 z-50 flex flex-col items-center justify-center gap-4 bg-bg-surface px-6 text-center\">\n <h1 className=\"text-h3 text-text-primary\">{APP_TITLE}</h1>\n {!waiting && (\n <>\n <p className=\"max-w-prose text-body-2 text-text-secondary\">{SHELL_COPY[state.reason]}</p>\n {isRecoverable(state.reason) && <Button onClick={onContinue}>{state.phase === 'authenticating' ? CONTINUE_LABEL : RETRY_LABEL}</Button>}\n </>\n )}\n </div>\n )\n}\n`,\n 'src/relay/client.ts': `import { signViaEstivaId } from '@estiva-app/identity'\nimport { browserOnlineSource, createLiveClientHolder, type LiveClient } from '@estiva-app/platform'\nimport { currentToken } from '../auth/estivaId'\nimport { ID_CONFIG, RELAY_URL } from '../config'\n\n/**\n * The event kinds this app reads. They are the app's own decision, and the one\n * blank a new app fills in: Ship reads seven (projects, issues, changes,\n * messages, comments, deletions and edits), Peek its own. Until there are kinds\n * here the app is connected and reads nothing, which is a correct state.\n */\nexport const KINDS: number[] = []\n\n/**\n * The tab's one relay client. \\`@estiva-app/platform\\` hands out a holder and\n * keeps nothing, so this line is where \"one connection per tab\" lives. The relay\n * signs a connection in once and caps subscriptions per connection, so a\n * connection per component is wrong, not only wasteful; React's StrictMode,\n * which mounts everything twice, is what would show it.\n *\n * Peek and Ship hold theirs the same way. Everything the client needs is handed\n * in and nothing here reads the app's own data, so it could move into\n * \\`@estiva-app/platform\\` as it is, if that is ever worth doing.\n */\nconst holder = createLiveClientHolder()\n\n/**\n * The client, or \\`null\\` when this build cannot have one: no relay is set, or\n * there is no sign-in. The relay refuses anything before sign-in, so a socket\n * without it would only connect, be refused and retry.\n */\nexport function relayClient(): LiveClient | null {\n const base = ID_CONFIG?.base\n if (!RELAY_URL || !base) return null\n return holder.get({\n relayUrl: RELAY_URL,\n // Read on every connect, never kept: a token kept from the first connect\n // outlives a silent renewal as a dead one, and the next reconnect fails.\n getCredential: () => {\n const token = currentToken()\n return token ? { accessToken: token.accessToken, pubkey: token.pubkey } : null\n },\n // \\`expectedPubkey\\` is a check, not a request. Estiva ID signs as the token's\n // owner whatever it is handed, so a mismatch comes back as a valid event\n // signed by somebody else. Always pass it.\n sign: (unsigned, token, expectedPubkey) => signViaEstivaId(unsigned, { base, token, expectedPubkey }),\n online: typeof window === 'undefined' ? undefined : browserOnlineSource(window),\n // Names this app in the relay's own logs.\n subscriptionPrefix: '${name}-',\n log: (message, detail) => console.debug('[relay]', message, detail ?? ''),\n })\n}\n`,\n 'src/relay/useRelayState.ts': `import type { RelayState } from '@estiva-app/protocol'\nimport { useSyncExternalStore } from 'react'\nimport { relayClient } from './client'\n\nconst subscribe = (onChange: () => void): (() => void) => relayClient()?.onState(onChange) ?? (() => {})\nconst snapshot = (): RelayState | 'off' => relayClient()?.state() ?? 'off'\n\n/** The connection's state, for a page to show: \\`'off'\\` when this build has no client. */\nexport function useRelayState(): RelayState | 'off' {\n return useSyncExternalStore(subscribe, snapshot, () => 'off')\n}\n`,\n 'src/relay/client.test.ts': `import { afterEach, describe, expect, it, vi } from 'vitest'\n\n/**\n * The tab's one relay connection. A fake socket stands in for the relay, so this\n * runs with no network, and counts the sockets opened.\n */\nclass FakeSocket {\n static opened = 0\n onopen: (() => void) | null = null\n onmessage: ((event: { data: unknown }) => void) | null = null\n onclose: (() => void) | null = null\n onerror: (() => void) | null = null\n constructor() {\n FakeSocket.opened += 1\n }\n send() {}\n close() {}\n}\n\nafterEach(() => {\n vi.unstubAllEnvs()\n vi.unstubAllGlobals()\n vi.resetModules()\n FakeSocket.opened = 0\n})\n\ndescribe('the relay client', () => {\n it('opens no connection when no relay is set', async () => {\n vi.stubEnv('VITE_RELAY_URL', '')\n vi.stubEnv('VITE_ESTIVA_ID_ORIGIN', 'http://localhost:8787')\n vi.stubEnv('VITE_ESTIVA_ID_CLIENT_ID', '${name}')\n vi.stubGlobal('WebSocket', FakeSocket)\n const { relayClient } = await import('./client')\n expect(relayClient()).toBeNull()\n expect(FakeSocket.opened).toBe(0)\n })\n\n it('opens no connection with a relay and no sign-in', async () => {\n vi.stubEnv('VITE_RELAY_URL', 'http://localhost:3000')\n vi.stubEnv('VITE_ESTIVA_ID_ORIGIN', '')\n vi.stubGlobal('WebSocket', FakeSocket)\n const { relayClient } = await import('./client')\n expect(relayClient()).toBeNull()\n expect(FakeSocket.opened).toBe(0)\n })\n\n it('holds one connection per tab, however often it is asked', async () => {\n vi.stubEnv('VITE_RELAY_URL', 'http://localhost:3000')\n vi.stubEnv('VITE_ESTIVA_ID_ORIGIN', 'http://localhost:8787')\n vi.stubEnv('VITE_ESTIVA_ID_CLIENT_ID', '${name}')\n vi.stubGlobal('WebSocket', FakeSocket)\n const { relayClient } = await import('./client')\n const first = relayClient()\n expect(first).not.toBeNull()\n expect(relayClient()).toBe(first)\n expect(FakeSocket.opened).toBe(1)\n first?.close()\n })\n})\n`,\n 'src/main.tsx': `import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport './index.css'\nimport { AuthShell } from './auth/AuthShell'\nimport { bootAuth, enterFromShell } from './auth/boot'\n\n// Settle who this load belongs to first, then render: the app itself is imported\n// only once sign-in is settled, so nothing in it evaluates as the wrong person.\nvoid (async () => {\n const root = createRoot(document.getElementById('root')!)\n const boot = await bootAuth()\n if (boot.kind === 'leaving') return\n if (boot.kind === 'shell') {\n root.render(\n <StrictMode>\n <AuthShell state={boot.state} onContinue={() => void enterFromShell()} />\n </StrictMode>,\n )\n return\n }\n const { App } = await import('./App')\n root.render(\n <StrictMode>\n <App />\n </StrictMode>,\n )\n})()\n`,\n 'src/App.tsx': `import { AppShell, IdentityMenu, NavItem, Sidebar, type Identity } from '@estiva-app/ui'\nimport { IconHome } from '@tabler/icons-react'\nimport { useEffect, useState } from 'react'\nimport { beginSignOut, currentToken, whoAmI } from './auth/estivaId'\nimport { APP_TITLE, ID_CONFIG, RELAY_URL } from './config'\nimport { HomePage } from './pages/HomePage'\nimport { useRelayState } from './relay/useRelayState'\n\n/** The frame: the package's AppShell with a sidebar, and the one page. */\nexport function App() {\n const signedIn = currentToken() !== null\n const [me, setMe] = useState<Identity>({})\n const relayState = useRelayState()\n\n useEffect(() => {\n if (!signedIn) return\n let live = true\n void whoAmI().then((who) => {\n if (live && who) setMe(who)\n })\n return () => {\n live = false\n }\n }, [signedIn])\n\n return (\n <AppShell\n logo={APP_TITLE}\n identity={<IdentityMenu me={me} signedIn={signedIn} idBase={ID_CONFIG?.base} onSignOut={signedIn ? beginSignOut : undefined} />}\n nav={\n <Sidebar>\n <NavItem href=\"/\" label=\"Home\" icon={<IconHome size={16} stroke={1.5} />} active />\n </Sidebar>\n }\n >\n <HomePage relay={RELAY_URL} state={relayState} name={me.name} />\n </AppShell>\n )\n}\n`,\n 'src/pages/HomePage.tsx': `import type { RelayState } from '@estiva-app/protocol'\nimport { EmptyState } from '@estiva-app/ui'\nimport { relayLabel } from '../config'\n\nexport interface HomePageProps {\n /** The relay this build is set to, or \\`null\\` when the app runs alone. */\n relay: string | null\n /** The connection's state: \\`'off'\\` when this build has no client. */\n state: RelayState | 'off'\n /** The signed-in person's name, once Estiva ID has said it. */\n name?: string\n}\n\nconst SAYS: Record<RelayState, (host: string, name?: string) => string> = {\n connecting: (host) => \\`Connecting to \\${host}\u2026\\`,\n authenticating: (host) => \\`Signing in to \\${host}\u2026\\`,\n live: (host, name) => (name ? \\`Connected to \\${host} as \\${name}.\\` : \\`Connected to \\${host}.\\`),\n reconnecting: (host) => \\`Reconnecting to \\${host}\u2026\\`,\n failed: (host) => \\`Could not connect to \\${host}.\\`,\n}\n\n/**\n * The first page. What it becomes is this app's own work; until then it says\n * whether the app is connected to the relay, and as whom.\n *\n * The empty state goes straight into the frame's \\`main\\`, with no box around it:\n * \\`main\\` is a flex column, so the empty state takes the room left and centres\n * in it both ways, as its own page says. A box around it would place it\n * instead, and no gate reads a box. The connection is a quiet caption after it,\n * at the foot of the page.\n */\nexport function HomePage({ relay, state, name }: HomePageProps) {\n const line = !relay\n ? 'Running alone: no relay is set. Set VITE_RELAY_URL in .env.local to connect.'\n : state === 'off'\n ? \\`Not connected: \\${relayLabel(relay)} needs sign-in, and this build has none.\\`\n : SAYS[state](relayLabel(relay), name)\n return (\n <>\n <EmptyState message=\"Nothing here yet.\" />\n <p className=\"px-6 pb-6 text-center text-caption text-text-muted\">{line}</p>\n </>\n )\n}\n`,\n 'src/pages/HomePage.stories.tsx': `import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { HomePage } from './HomePage'\n\nconst meta = {\n title: 'Pages/Home',\n component: HomePage,\n parameters: { layout: 'fullscreen' },\n // Stands in for the frame's main: a flex column the height of the screen,\n // which is what the page is drawn into in the app.\n decorators: [\n (Story) => (\n <div className=\"flex h-screen flex-col\">\n <Story />\n </div>\n ),\n ],\n} satisfies Meta<typeof HomePage>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n/** No relay set: the app runs alone, the way it starts. */\nexport const RunningAlone: Story = { args: { relay: null, state: 'off' } }\n\n/** A relay set, before it has signed the connection in. */\nexport const Connecting: Story = { args: { relay: 'http://localhost:3000', state: 'connecting' } }\n\n/** Connected, as the person who signed in. */\nexport const Connected: Story = { args: { relay: 'http://localhost:3000', state: 'live', name: 'Alex Kim' } }\n`,\n 'src/App.test.tsx': `import { render, screen } from '@testing-library/react'\nimport { describe, expect, it } from 'vitest'\nimport { App } from './App'\nimport { APP_TITLE } from './config'\n\ndescribe('${title}', () => {\n it('opens in its frame, anonymous in a build with no sign-in', () => {\n render(<App />)\n expect(screen.getAllByText(APP_TITLE).length).toBeGreaterThan(0)\n expect(screen.getByText('Nothing here yet.')).toBeTruthy()\n expect(screen.getByText(/^Running alone/)).toBeTruthy()\n expect(screen.getByRole('link', { name: 'Home' })).toBeTruthy()\n })\n})\n`,\n\n '.storybook/main.ts': `import type { StorybookConfig } from '@storybook/react-vite'\n\nconst config: StorybookConfig = {\n framework: '@storybook/react-vite',\n stories: ['../src/**/*.stories.@(ts|tsx)'],\n addons: ['@storybook/addon-docs'],\n}\n\nexport default config\n`,\n '.storybook/preview.tsx': `import type { Preview } from '@storybook/react-vite'\nimport '../src/index.css'\n\n// The app's one theme, selected the way the app selects it: on <html>.\ndocument.documentElement.dataset.theme = ${JSON.stringify(theme)}\n\nconst preview: Preview = {\n // Every story file gets a Docs page, as in Peek and Ship: the catalogue links each\n // part to it, and \\`npm run registry:check\\` in CI's job \\`gate\\` fails a link that\n // leads nowhere.\n tags: ['autodocs'],\n parameters: { layout: 'centered' },\n}\n\nexport default preview\n`,\n\n 'README.md': `# ${title}\n\nAn Estiva app, made with \\`create-estiva-app\\` from \\`@estiva-app/ui\\`. It starts\nwith the package's sidebar frame, one theme (\\`${theme}\\`), sign-in with Estiva ID,\nand every UI Guardrails gate on, at zero.\n\n## Run it\n\n\\`\\`\\`sh\nnpm install\nnpm run dev\n\\`\\`\\`\n\nWith no settings it runs **anonymous**: no sign-in is offered, and nothing reaches\nthe real Estiva ID. To sign in, copy \\`.env.example\\` to \\`.env.local\\` and fill it in.\nThe app must first be registered with that Estiva ID as its own app.\n\n## The relay\n\n${title} is connected to the relay, the workspace, from its first commit, as\nwhoever signed in. \\`src/relay/client.ts\\` holds the tab's one connection: ask it\nfor \\`relayClient()\\`, and never open a socket of your own. The home page shows the\nconnection's state.\n\nWith \\`VITE_RELAY_URL\\` empty it runs alone and opens no connection. The relay\nalso needs sign-in, so with a relay and no sign-in there is no connection either.\n\nThree packages come with it: \\`@estiva-app/protocol\\` (the wire: events, ids,\nsigning, the relay clients), \\`@estiva-app/platform\\` (the one connection a tab\nholds) and \\`@estiva-app/interop\\` (showing another app's objects, from the\nmanifest that app publishes).\n\n## What you fill in\n\n1. \\`.env.local\\`: Estiva ID and the relay (see \\`.env.example\\`).\n2. \\`KINDS\\` in \\`src/relay/client.ts\\`: the event kinds ${title} reads. Until then\n it is connected and reads nothing.\n3. The product: its pages, and the fold that turns what arrives on the\n connection into the app's state.\n\nBefore ${title} can sign in on the real Estiva ID it has to be registered there\nas its own app, with:\n\n- its exact redirect address\n- every event kind it will sign, **including 22242**, the relay's sign-in handshake\n- the relay's address, allowed for that handshake. Estiva ID signs a handshake\n only for a relay both the app and the deployment allow.\n\nAnything left out fails at the very last step: every screen looks right, and\nnothing arrives.\n\n## The checks\n\n| command | what |\n|---|---|\n| \\`npm run typecheck\\` | TypeScript, the app and its settings files |\n| \\`npm run lint\\` | everything: TypeScript's and React's rules, the token contract, the gate |\n| \\`npm run lint:rules\\` | the gate alone, and \\`.gates-count.json\\` |\n| \\`npm test\\` | the tests |\n| \\`npm run build\\` | the build |\n| \\`npm run gates:status\\` | which gates are on, read from the code |\n| \\`npm run ui:find <words>\\` | what the package and this app already have for it |\n| \\`npm run registry:check\\` | every part says what it is for |\n| \\`npx estiva-gates ci\\` | every step of CI's job \\`gate\\`: the gate, its count committed, the tokens, the catalogue, the hook |\n| \\`npm run storybook\\` | the stories |\n\nThe gates are the package's, imported rather than copied, so a rule written later\narrives with an ordinary version bump. A Claude session started in this folder is\nstopped before it writes code the gate refuses (\\`.claude/settings.json\\`).\n\n## Once it is on GitHub\n\nRequire the check \\`gate\\` before anything merges into \\`main\\`: a ruleset on \\`main\\`\n(Settings \u2192 Rules \u2192 Rulesets) with \"Require status checks to pass\", the check\n\\`gate\\`, and no one allowed to bypass it. Until then \\`npm run gates:status\\` shows\nUIG-6 as not done.\n`,\n 'CLAUDE.md': `# ${title}, for Claude Code\n\n${title} is an Estiva app. Its parts, tokens and gates come from \\`@estiva-app/ui\\`.\n\n**Use the package's parts, never a raw element or a hand-built look.** The gate\nrefuses code that reaches past the package, and each refusal names what to use\ninstead. What it refuses is the package's to say, and grows with it: the\n\\`estiva-ui\\` skill lists it. It runs before you write\n(the hook in \\`.claude/settings.json\\`, for a session started in this folder), in\n\\`npm run lint:rules\\` and in CI. A session started elsewhere: run\n\\`npm run lint:rules\\` after changing \\`src/\\`, and fix what it reports. Keep something\nonly with its reason on the line above, \\`// @estiva-escape(<rule>): <reason>\\`, never with\n\\`eslint-disable\\`.\n\n**Tokens only.** Colours, type, corners and shadows come from the package's preset.\n\n**One relay connection per tab.** \\`relayClient()\\` in \\`src/relay/client.ts\\` is the\nconnection. Never open a socket and never make a second holder: the relay signs a\nconnection in once, and caps subscriptions per connection.\n\n**Look before you build.** \\`npm run ui:find <what it does>\\` searches the package's\nparts and this app's own. Use what it finds.\n\n**Every part says what it is for.** A new part gets a one-line \\`/** \u2026 */\\` comment\ndirectly above it. \\`npm run registry:check\\` and CI's job \\`gate\\` refuse a part without one.\n\n**The count starts at zero and stays there** (\\`.gates-count.json\\`, \\`docs/GATES-DEBT.md\\`).\n\nWhat each gate is and how it is wired: the package's README,\n\\`node_modules/@estiva-app/ui/README.md\\`.\n`,\n }\n}\n\n/** The versions of what this package does not use itself, from the npm registry. */\nexport function askNpm(names: string[]): Record<string, string> {\n const npm = process.env.npm_execpath\n const out: Record<string, string> = {}\n for (const name of names) {\n const args = ['view', name, 'version']\n const version = (npm ? execFileSync(process.execPath, [npm, ...args]) : execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { shell: process.platform === 'win32' }))\n .toString()\n .trim()\n if (!/^\\d+\\.\\d+\\.\\d+/.test(version)) throw new Error(`npm did not say which version of ${name} is current: \"${version}\"`)\n out[name] = `^${version}`\n }\n return out\n}\n\n/** Write the app. Refuses a folder that already exists. Returns the folder. */\nexport function createApp(options: CreateAppOptions): string {\n const dir = resolve(options.parent ?? process.cwd(), options.name)\n if (existsSync(dir)) throw new Error(`${dir} already exists: create-estiva-app never writes into a folder that is there`)\n const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')) as PackageJson\n const missing = ASKED_OF_NPM.filter((n) => !pkg.devDependencies[n] && !options.versions?.[n])\n const versions = { ...(missing.length ? askNpm(missing) : {}), ...options.versions }\n const files = appFiles({ ...options, versions })\n for (const [rel, text] of Object.entries(files)) {\n const path = join(dir, rel)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, text)\n }\n return dir\n}\n", "/**\n * The names of the app rules, with nothing imported.\n *\n * `create-estiva-app` needs these names to write a new app's count at zero, and\n * nothing else of the plugin. It runs from `npx` in an empty folder, where only\n * this package's own dependencies are installed, so it must never load the\n * rules themselves: since `no-copied-look` (UIG-25, 0.34.0) they import\n * TypeScript, and that import stopped the command before it wrote a file\n * (UIG-10, reopened 27 September). `index.ts` holds its rule table to exactly\n * these names, so the two cannot drift.\n */\n\n/** The name the configs register the plugin under, so every rule id is `estiva/<rule>`. */\nexport const PLUGIN_KEY = 'estiva'\n\nexport const APP_RULE_NAMES = [\n 'no-raw-element',\n 'no-rebuilt-behaviour',\n 'no-restyled-part',\n 'no-handmade-header',\n 'no-handmade-empty-state',\n 'no-native-title',\n 'no-copied-look',\n 'no-field-outside-a-form',\n] as const\n\nexport type AppRuleName = (typeof APP_RULE_NAMES)[number]\n\n/** The ids `recommended` and `strict` carry \u2014 what an app's gate runs and counts. */\nexport const APP_RULE_IDS = APP_RULE_NAMES.map((name) => `${PLUGIN_KEY}/${name}`)\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createApp
4
- } from "./chunk-DZVDOJGV.js";
4
+ } from "./chunk-FIBZLYQI.js";
5
5
  import "./chunk-YKZXKVR2.js";
6
6
 
7
7
  // src/gates/create-app-cli.ts
@@ -27,7 +27,7 @@ import {
27
27
  askNpm,
28
28
  createApp,
29
29
  themes
30
- } from "./chunk-DZVDOJGV.js";
30
+ } from "./chunk-FIBZLYQI.js";
31
31
  import "./chunk-YKZXKVR2.js";
32
32
 
33
33
  // src/gates/gate-config.ts
package/dist/index.js CHANGED
@@ -2509,7 +2509,7 @@ function DialogShell({ title, onClose, headerContent, footer, children, bodyClas
2509
2509
  /* @__PURE__ */ jsx37(
2510
2510
  ContainerHeader,
2511
2511
  {
2512
- title: headerContent ?? /* @__PURE__ */ jsx37(Parts.Title, { className: "text-h4 text-text-primary", render: /* @__PURE__ */ jsx37("span", {}), children: title }),
2512
+ title: headerContent ?? /* @__PURE__ */ jsx37(Parts.Title, { className: "block truncate text-h4 text-text-primary", render: /* @__PURE__ */ jsx37("span", {}), children: title }),
2513
2513
  actions: (
2514
2514
  /*
2515
2515
  The ✕ IS the `Close` part now, rather than a button that calls