your-ai-workflow-firebase-os 1.2.55 → 1.2.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ export { useConfig } from './lib/ConfigContext';
6
6
  export type { FirebaseOSConfig } from './lib/ConfigContext';
7
7
  export { db, auth, storage } from './lib/firebase';
8
8
  export { ContactPopup } from './components/ContactPopup';
9
+ export { notify } from './lib/notify';
10
+ export type { NotifyInput } from './lib/notify';
9
11
  export { registerMicrocomponent } from './microcomponents/registry';
10
12
  export { Button } from './components/Button';
11
13
  export { Card } from './components/Card';
package/dist/vite.cjs.js CHANGED
@@ -81,10 +81,10 @@ createRoot(document.getElementById('root')!).render(
81
81
  <App />
82
82
  </StrictMode>,
83
83
  )
84
- `,c=`src/your-ai-workflow-firebase-os`,l=`${c}/configs`,u=`${c}/microcomponents`,d=[`theme.config.ts`,`contactForm.config.ts`,`supportForm.config.ts`,`registry.ts`,`pagesRegistry.ts`,`home.config.ts`],f=e=>e.split(n.sep).join(`/`),p=e=>n.basename(e).replace(`.config.ts`,``).replace(/Tab$/,``).toLowerCase(),m=e=>n.basename(e).replace(`.config.ts`,``).replace(/Page$/,``).toLowerCase(),h=e=>n.basename(e).replace(`.config.ts`,``).replace(/Form$/,``).toLowerCase(),g=e=>{let t=n.basename(e).replace(/\.tsx$/,``).replace(/Content$/,``).replace(/([a-z0-9])([A-Z])/g,`$1_$2`).toLowerCase();return[t,t.replace(/_/g,``)]},_=e=>n.basename(e).replace(/\.config\.ts$/,``);function v(e){let t=n.basename(e);return d.includes(t)?`shipped`:/Tab\.config\.ts$/.test(t)?`tab`:/Page\.config\.ts$/.test(t)?`page`:/Form\.config\.ts$/.test(t)?`form`:`other`}var y=e=>t.existsSync(e)?t.readdirSync(e,{withFileTypes:!0}).filter(e=>e.isFile()).map(e=>e.name).sort():[],b=e=>t.existsSync(n.join(e,c));function x(e,t){let r=f(n.relative(e,t));return r.startsWith(l+`/`)||r.startsWith(u+`/`)}function S(e){let t=[],r=n.join(e,l);for(let e of y(r)){if(!/\.tsx?$/.test(e))continue;let i=n.join(r,e),a=`${l}/${e}`,o=v(e);o===`tab`?t.push({kind:o,file:a,abs:i,name:e,id:p(e)}):o===`page`?t.push({kind:o,file:a,abs:i,name:e,id:m(e)}):o===`form`||/Form\.config\.ts$/.test(e)?t.push({kind:`form`,file:a,abs:i,name:e,id:h(e)}):t.push({kind:`other`,file:a,abs:i,name:e,id:``})}let i=n.join(e,u);for(let e of y(i))e.endsWith(`.tsx`)&&e.replace(/\.tsx$/,``)!==`registry`&&t.push({kind:`microcomponent`,file:`${u}/${e}`,abs:n.join(i,e),name:e,id:g(e)[0]});return t}var C=e=>e.filter(e=>e.kind!==`other`),w=[{test:e=>/Content\.tsx$/.test(e),home:u},{test:e=>/(Tab|Page|Form)\.config\.tsx?$/.test(e),home:l}],ee=new Set([`node_modules`,`.git`,`dist`,`build`]);function T(e){let r=[],i=n.join(e,`src`),a=(i,o)=>{if(!(o>6||!t.existsSync(i)))for(let s of t.readdirSync(i,{withFileTypes:!0})){let t=n.join(i,s.name),c=f(n.relative(e,t));if(s.isDirectory()){ee.has(s.name)||a(t,o+1);continue}if(!(c===`${l}/${s.name}`||c===`${u}/${s.name}`)){for(let{test:e,home:t}of w)if(e(s.name)){r.push(`${c}: files like this are only found directly in ${t}/ — move it to ${t}/${s.name}.`);break}}}};return a(i,0),r.sort()}var E=e=>{let t=n.basename(e);return w.some(e=>e.test(t))},D=e=>JSON.stringify(e);function O(e){let t=[],n=[],r=[],i=[];return C(e).forEach((e,a)=>{let o=`m${a}`;if(t.push(`import * as ${o} from ${D(f(e.abs))};`),e.kind===`tab`)n.push(`{
84
+ `,c=`src/your-ai-workflow-firebase-os`,l=`${c}/configs`,u=`${c}/microcomponents`,d=`${c}/Home.tsx`,f=[`theme.config.ts`,`contactForm.config.ts`,`supportForm.config.ts`,`registry.ts`,`pagesRegistry.ts`,`home.config.ts`],p=e=>e.split(n.sep).join(`/`),m=e=>n.basename(e).replace(`.config.ts`,``).replace(/Tab$/,``).toLowerCase(),h=e=>n.basename(e).replace(`.config.ts`,``).replace(/Page$/,``).toLowerCase(),g=e=>n.basename(e).replace(`.config.ts`,``).replace(/Form$/,``).toLowerCase(),_=e=>{let t=n.basename(e).replace(/\.tsx$/,``).replace(/Content$/,``).replace(/([a-z0-9])([A-Z])/g,`$1_$2`).toLowerCase();return[t,t.replace(/_/g,``)]},v=e=>n.basename(e).replace(/\.config\.ts$/,``);function y(e){let t=n.basename(e);return f.includes(t)?`shipped`:/Tab\.config\.ts$/.test(t)?`tab`:/Page\.config\.ts$/.test(t)?`page`:/Form\.config\.ts$/.test(t)?`form`:`other`}var b=e=>t.existsSync(e)?t.readdirSync(e,{withFileTypes:!0}).filter(e=>e.isFile()).map(e=>e.name).sort():[],x=e=>t.existsSync(n.join(e,c));function S(e,t){let r=p(n.relative(e,t));return r.startsWith(l+`/`)||r.startsWith(u+`/`)||r===d}function C(e){let r=[],i=n.join(e,l);for(let e of b(i)){if(!/\.tsx?$/.test(e))continue;let t=n.join(i,e),a=`${l}/${e}`,o=y(e);o===`tab`?r.push({kind:o,file:a,abs:t,name:e,id:m(e)}):o===`page`?r.push({kind:o,file:a,abs:t,name:e,id:h(e)}):o===`form`||/Form\.config\.ts$/.test(e)?r.push({kind:`form`,file:a,abs:t,name:e,id:g(e)}):r.push({kind:`other`,file:a,abs:t,name:e,id:``})}let a=n.join(e,u);for(let e of b(a))e.endsWith(`.tsx`)&&e.replace(/\.tsx$/,``)!==`registry`&&r.push({kind:`microcomponent`,file:`${u}/${e}`,abs:n.join(a,e),name:e,id:_(e)[0]});let o=n.join(e,d);return t.existsSync(o)&&r.push({kind:`other`,file:d,abs:o,name:`Home.tsx`,id:``}),r}var w=e=>e.filter(e=>e.kind!==`other`),T=[{test:e=>/Content\.tsx$/.test(e),home:u},{test:e=>/(Tab|Page|Form)\.config\.tsx?$/.test(e),home:l}],E=new Set([`node_modules`,`.git`,`dist`,`build`]);function D(e){let r=[],i=n.join(e,`src`),a=(i,o)=>{if(!(o>6||!t.existsSync(i)))for(let s of t.readdirSync(i,{withFileTypes:!0})){let t=n.join(i,s.name),c=p(n.relative(e,t));if(s.isDirectory()){E.has(s.name)||a(t,o+1);continue}if(!(c===`${l}/${s.name}`||c===`${u}/${s.name}`)){for(let{test:e,home:t}of T)if(e(s.name)){r.push(`${c}: files like this are only found directly in ${t}/ — move it to ${t}/${s.name}.`);break}}}};return a(i,0),r.sort()}var O=e=>{let t=n.basename(e);return T.some(e=>e.test(t))},k=e=>JSON.stringify(e);function A(e){let t=[],n=[],r=[],i=[];return w(e).forEach((e,a)=>{let o=`m${a}`;if(t.push(`import * as ${o} from ${k(p(e.abs))};`),e.kind===`tab`)n.push(`{
85
85
  const configVal = pickConfig(${o});
86
86
  if (configVal) {
87
- const tabId = ${D(e.id)};
87
+ const tabId = ${k(e.id)};
88
88
  const finalTab = {
89
89
  config: {
90
90
  ...configVal,
@@ -102,7 +102,7 @@ createRoot(document.getElementById('root')!).render(
102
102
  }`);else if(e.kind===`page`)n.push(`{
103
103
  const configVal = pickConfig(${o});
104
104
  if (configVal) {
105
- const pageId = ${D(e.id)};
105
+ const pageId = ${k(e.id)};
106
106
  const finalPage = {
107
107
  config: {
108
108
  ...configVal,
@@ -120,19 +120,19 @@ createRoot(document.getElementById('root')!).render(
120
120
  }`);else if(e.kind===`form`)r.push(`{
121
121
  const configVal = pickConfig(${o});
122
122
  if (configVal) {
123
- const formId = ${D(e.id)};
123
+ const formId = ${k(e.id)};
124
124
  // The form's id is its file name; systemName always follows the file.
125
125
  const finalConfig = { ...configVal, systemName: formId };
126
126
  forms[formId] = finalConfig;
127
127
  forms[finalConfig.systemName] = finalConfig;
128
128
  forms[finalConfig.systemName.toLowerCase()] = finalConfig;
129
129
  }
130
- }`);else if(e.kind===`microcomponent`){let[t,n]=g(e.name);i.push(`if (${o} && ${o}.default) {
131
- microcomponents[${D(t)}] = ${o}.default;
130
+ }`);else if(e.kind===`microcomponent`){let[t,n]=_(e.name);i.push(`if (${o} && ${o}.default) {
131
+ microcomponents[${k(t)}] = ${o}.default;
132
132
  // Also answer to the squashed form ("mobile_apps" -> "mobileapps"): a page id
133
133
  // derived from a file name (mobileAppsPage.config.ts -> "mobileapps") has no
134
134
  // underscores, and the page must still find MobileAppsContent.tsx.
135
- microcomponents[${D(n)}] = ${o}.default;
135
+ microcomponents[${k(n)}] = ${o}.default;
136
136
  }`)}}),`// Generated by Your AI Firebase from the files in ${c}/.
137
137
  // Never written to disk; never edit. Add or remove a file and it updates itself.
138
138
  ${t.join(`
@@ -173,4 +173,4 @@ if (typeof window !== 'undefined') {
173
173
  export const pagesRegistry = pages;
174
174
  export const formsRegistry = forms;
175
175
  export const microcomponentRegistry = microcomponents;
176
- `}var te=e=>O(S(e)),k=e=>e.split(`/`).pop()||e,A=e=>/\/configs\/[^/]+$/.test(e),j=e=>/\/microcomponents\/[^/]+\.tsx$/.test(e),M=e=>e.replace(/\/\*[\s\S]*?\*\//g,``).replace(/(^|[^:\\'"`])\/\/.*$/gm,`$1`),N=(e,t)=>{let n=RegExp(`(?:^|[\\s,{])${t}\\s*:\\s*([^,\\n}]+)`,`g`),r=[],i;for(;i=n.exec(e);)r.push(i[1].trim());return r},P=e=>{let t=e.match(/^(['"`])([^'"`]*)\1$/);return t?t[2]:null},F=e=>/export\s+(?:const|let|var|function|class)\s+\w*Config\b/.test(e)||/export\s*\{[^}]*\b\w*Config\b[^}]*\}/.test(e),I=e=>/export\s+default\b/.test(e),L=e=>e.toLowerCase().replace(/[^a-z0-9]+/g,`_`).replace(/^_|_$/g,``),R=[`board`,`table`,`calendar`,`drive`],z={board:{call:`actions.openInlineEditor(categoryId)`,token:`openInlineEditor`},table:{call:`actions.addRow()`,token:`addRow`},calendar:{call:`actions.openAddModal()`,token:`openAddModal`},drive:{call:`the openUploadModal() prop`,token:`openUploadModal`}},B=/(?:copy (?:this|the sentence|it) (?:in)?to|paste (?:this|it) (?:in)?to|copy for (?:your |the )?(?:ai|assistant)|ask your assistant|your assistant.{0,3}s chat|copy this to change)[^\n]{0,40}/i,V=e=>{let t=N(e,`route`);return{first:t[0],count:t.length}};function H(e){let t=[],n=[],r=new Map,i=new Set;for(let{file:t}of e)if(j(t))for(let e of g(k(t)))i.add(e);let a=new Map;for(let{file:t,source:n}of e){if(!A(t))continue;let e=k(t),r=v(e);if(r!==`tab`&&r!==`page`)continue;let i=M(n),o=N(i,`template`)[0],s=o===void 0?null:P(o);if(!s||!R.includes(s))continue;let c=N(i,r===`tab`?`tabName`:`pageName`)[0],l=c===void 0?null:P(c),u=[r===`tab`?p(e):m(e),...l?[L(l),L(l).replace(/_/g,``)]:[]];for(let e of u)a.set(e,s)}for(let{file:o,source:s}of e){let e=k(o),c=M(s);if(A(o)){let n=v(e);if(n===`shipped`)continue;if(n===`other`){t.push(`${o}: rename it to somethingTab.config.ts (a dashboard page), somethingPage.config.ts (a page for anyone) or somethingForm.config.ts (a form).`);continue}if(!F(c)&&!I(c)&&t.push(`${o}: export one const named ${_(e)}Config.`),n===`tab`||n===`page`){let{first:a,count:s}=V(c),l=a===void 0?null:P(a);if(s===0||l===null||!l.startsWith(`/`))t.push(`${o}: route must be one string starting with /, e.g. route: '/onboarding'. A page has exactly one web address.`);else{let e=r.get(l);e?t.push(`${o}: route '${l}' is already used by ${e}. One page, one file, one address.`):r.set(l,o)}let u=N(c,`template`)[0],d=u===void 0?null:P(u);if(!d||![`board`,`table`,`calendar`,`drive`,`inline_form`,`popup_form`].includes(d)){let r=n===`tab`?p(e):m(e),a=N(c,n===`tab`?`tabName`:`pageName`)[0],s=a===void 0?null:P(a),l=e=>e.toLowerCase().replace(/[^a-z0-9]+/g,`_`).replace(/^_|_$/g,``);if(![r,...s?[l(s),l(s).replace(/_/g,``)]:[]].some(e=>i.has(e))){let n=_(e).replace(/(Tab|Page)$/,``),r=(s||n).split(/[^a-zA-Z0-9]+/).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(``);t.push(`${o}: no content file matches this page, so it renders empty — create src/your-ai-workflow-firebase-os/microcomponents/${r}Content.tsx with export default.`)}}}if(n===`tab`){let e=N(c,`pageType`)[0],n=e===void 0?null:P(e);n!==`user`&&n!==`internal`&&n!==`admin`&&t.push(`${o}: pageType must be 'user', 'internal' or 'admin'. A dashboard page is never public.`)}if(n===`form`){let n=h(e),r=N(c,`systemName`)[0];r!==void 0&&P(r)!==n&&t.push(`${o}: systemName must be '${n}'.`)}continue}if(j(o)){I(c)||t.push(`${o}: add export default.`);let r=g(e).map(e=>a.get(e)).find(Boolean),i=/<form\b/.test(c)||/\bonSubmit\b/.test(c);if(i&&r){let e=z[r];c.includes(e.token)||n.push(`${o}: this page is a ${r} and already has its own add and edit dialog, themed for light and dark — open it with ${e.call} instead of building a form here.`)}else i&&!/ContactPopup/.test(c)&&n.push(`${o}: forms are *Form.config.ts files shown with <ContactPopup formId="…" />; a hand-written form never reaches the Inbox.`);/<style[\s>]/.test(c)&&t.push(`${o}: a <style> tag is global CSS and restyles every page of the app — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);let s=c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);s&&n.push(`${o}: ${s[0]} makes the page exactly one screen tall and strands everything below it on a phone — the page is already inside a panel that scrolls, so let it be as long as it needs and drop the height and any overflow-hidden around it.`);let l=c.match(B);l&&n.push(`${o}: "${l[0]}" is wording off the owner's admin screens — the page is for its visitors, not for the owner or for you. Delete it and any prompt or copy-this box on the page.`);let u=c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);u&&n.push(`${o}: ${u[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`)}}return{errors:t,warnings:n}}var U=[`--bg-color`,`--bg-secondary-color`,`--fg-color`,`--accent-color`,`--panel-bg`,`--panel-border`,`--button-bg`,`--button-fg`,`--button-hover-bg`,`--btn-sec-bg`,`--btn-sec-border`,`--btn-sec-text`,`--text-gradient`,`--glow-color`,`--notification-icon-color`,`--selection-bg`,`--selection-text`],ne=(e,t)=>{let n=[],r=t.replace(/\/\*[\s\S]*?\*\//g,``);for(let[,t,i]of r.matchAll(/([^{}]+)\{([^{}]*)\}/g)){let r=t.trim(),a=U.filter(e=>RegExp(`${e}\\s*:[^;}]*!important`).test(i));if(a.length){n.push(`${e}: \`${a[0]}\` is set with \`!important\` here, and the app writes that colour itself from the owner's theme — forcing it freezes it, so their Theme screen stops working. Delete the \`!important\` and set the colour in configs/theme.config.ts instead, in background, backgroundSecondary, foreground, accent or buttonText; every other colour is derived from those five.`);continue}let o=/(?:^|[\s;])(?:font-size|line-height|font)\s*:/.test(i);if(/\.page-title\b/.test(r)){n.push(`${e}: the rule for \`${r.replace(/\s+/g,` `)}\` styles the app's page title, which is the library's chrome and not this file's to restyle — delete the rule; a page heading you want to size is one you write yourself inside a content file.`);continue}let s=i.match(/(?:^|[\s;])font-family\s*:\s*([^;}]+)/);if(s&&!/^\s*['"]?Inter['"]?\s*(?:,|$)/i.test(s[1])){n.push(`${e}: the rule for \`${r.replace(/\s+/g,` `)}\` sets a \`font-family\`, and the app has one typeface — Inter, everywhere, with no setting for it. Delete the \`font-family\` line; the rest of the rule can stay.`);continue}let c=r.split(`,`).map(e=>e.trim()).filter(e=>/^h[1-3]$/.test(e));c.length&&o&&n.push(`${e}: this rule sets a size on \`${c.join(`, `)}\`, which changes every page title in the app — delete the size (font-weight here is fine) and size the headings you write inside your own content file instead.`)}return n};function W(e,t){return H(e.map(e=>({file:e.file,source:t(e.abs)})))}var G=i,K=`\0`+G,q=[{rel:`src/App.tsx`,source:a},{rel:`src/main.tsx`,source:s}],J=e=>`${e} is owned by Your AI Firebase; your edit was discarded.`,re=e=>e.replace(/[?#].*$/,``),Y=e=>n.normalize(e).split(n.sep).join(`/`),X={info:e=>console.log(e),warn:e=>console.warn(e),error:e=>console.error(e)};function Z(e,r=X){let i=[];for(let{rel:a,source:o}of q){let s=n.join(e,a),c=null;try{c=t.readFileSync(s,`utf8`)}catch{c=null}c!==o&&(t.mkdirSync(n.dirname(s),{recursive:!0}),t.writeFileSync(s,o),i.push(a),c!==null&&r.warn(J(a)))}return i}function Q(e){let r=n.join(e,u,`registry.ts`);return t.existsSync(r)?!1:(t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,o),!0)}var $=(e,t)=>{let r=Y(re(t));for(let{rel:t,source:i}of q)if(r===Y(n.join(e,t)))return i;return null};function ie(){let e=process.cwd(),r=!1,i=`build`,a=X,o=()=>{let r=W(S(e),e=>t.readFileSync(e,`utf8`)),i=n.join(e,`src`,`your-ai-workflow-firebase-os`,`theme.css`),a=t.existsSync(i)?ne(`src/your-ai-workflow-firebase-os/theme.css`,t.readFileSync(i,`utf8`)):[];return{...r,errors:[...r.errors,...T(e),...a]}};return{name:`your-ai-firebase`,enforce:`pre`,config:()=>({resolve:{dedupe:[`firebase`,`react`,`react-dom`,`react-router-dom`]}}),configResolved(t){e=t.root,i=t.command,a=t.logger,r=b(e),r&&(Z(e,a),Q(e))},resolveId(e){return e===G?K:null},buildStart(){if(!r)return;let{errors:e,warnings:t}=o();for(let e of t)this.warn(e);if(e.length!==0)if(i===`build`)this.error(e[0]);else for(let t of e)a.error(t)},load(t){if(!r)return null;if(t===K){let t=S(e),{errors:n}=o();return n.length&&this.error(n[0]),O(t)}return $(e,t)},configureServer(t){if(!r)return;let n=()=>{let e=t.moduleGraph.getModuleById(K);e&&t.moduleGraph.invalidateModule(e),t.ws.send({type:`full-reload`,path:`*`})},i=r=>{let i=Y(r);if($(e,i)!==null){Z(e,t.config.logger);return}if(!x(e,i)&&!E(i))return;n();let{errors:a,warnings:s}=o();for(let e of s)t.config.logger.warn(e);for(let e of a)t.config.logger.error(e)};t.watcher.on(`add`,i),t.watcher.on(`unlink`,i),t.watcher.on(`change`,i)}}}exports.CANONICAL_APP_TSX=a,exports.CANONICAL_MAIN_TSX=s,exports.CONFIGS_DIR=l,exports.CONSUMER_DIR=c,exports.CONSUMER_REGISTRY_TS=o,exports.MICROCOMPONENTS_DIR=u,exports.OWNED_BANNER=r,exports.RESOLVED_VIRTUAL_ID=K,exports.SHIPPED_CONFIG_FILES=d,exports.VIRTUAL_ID=G,exports.VIRTUAL_REGISTRIES_ID=i,exports.classifyConfigFile=v,exports.discover=S,exports.ensureConsumerRegistry=Q,exports.fileIdFromConfigFile=_,exports.formIdFromFile=h,exports.isConsumerRoot=b,exports.isDiscoveredPath=x,exports.microcomponentKeysFromFile=g,exports.ownedMessage=J,exports.pageIdFromFile=m,exports.registrable=C,exports.restoreOwnedFiles=Z,exports.stripComments=M,exports.tabIdFromFile=p,exports.validate=H,exports.validateDiscovered=W,exports.virtualModuleSource=te,exports.virtualModuleSourceFor=O,exports.yourAiFirebase=ie;
176
+ `}var j=e=>A(C(e)),M=e=>e.split(`/`).pop()||e,N=e=>/\/configs\/[^/]+$/.test(e),P=e=>/\/microcomponents\/[^/]+\.tsx$/.test(e),F=e=>e.replace(/\/\*[\s\S]*?\*\//g,``).replace(/(^|[^:\\'"`])\/\/.*$/gm,`$1`),I=(e,t)=>{let n=RegExp(`(?:^|[\\s,{])${t}\\s*:\\s*([^,\\n}]+)`,`g`),r=[],i;for(;i=n.exec(e);)r.push(i[1].trim());return r},L=e=>{let t=e.match(/^(['"`])([^'"`]*)\1$/);return t?t[2]:null},R=e=>/export\s+(?:const|let|var|function|class)\s+\w*Config\b/.test(e)||/export\s*\{[^}]*\b\w*Config\b[^}]*\}/.test(e),z=e=>/export\s+default\b/.test(e),B=e=>e.toLowerCase().replace(/[^a-z0-9]+/g,`_`).replace(/^_|_$/g,``),V=[`board`,`table`,`calendar`,`drive`],H={board:{call:`actions.openInlineEditor(categoryId)`,token:`openInlineEditor`},table:{call:`actions.addRow()`,token:`addRow`},calendar:{call:`actions.openAddModal()`,token:`openAddModal`},drive:{call:`the openUploadModal() prop`,token:`openUploadModal`}},ee=/(?:copy (?:this|the sentence|it) (?:in)?to|paste (?:this|it) (?:in)?to|copy for (?:your |the )?(?:ai|assistant)|ask your assistant|your assistant.{0,3}s chat|copy this to change)[^\n]{0,40}/i,te=e=>{let t=I(e,`route`);return{first:t[0],count:t.length}};function U(e){let t=[],n=[],r=new Map,i=new Set;for(let{file:t}of e)if(P(t))for(let e of _(M(t)))i.add(e);let a=new Map;for(let{file:t,source:n}of e){if(!N(t))continue;let e=M(t),r=y(e);if(r!==`tab`&&r!==`page`)continue;let i=F(n),o=I(i,`template`)[0],s=o===void 0?null:L(o);if(!s||!V.includes(s))continue;let c=I(i,r===`tab`?`tabName`:`pageName`)[0],l=c===void 0?null:L(c),u=[r===`tab`?m(e):h(e),...l?[B(l),B(l).replace(/_/g,``)]:[]];for(let e of u)a.set(e,s)}for(let{file:o,source:s}of e){let e=M(o),c=F(s);if(N(o)){let n=y(e);if(n===`shipped`)continue;if(n===`other`){t.push(`${o}: rename it to somethingTab.config.ts (a dashboard page), somethingPage.config.ts (a page for anyone) or somethingForm.config.ts (a form).`);continue}if(!R(c)&&!z(c)&&t.push(`${o}: export one const named ${v(e)}Config.`),n===`tab`||n===`page`){let{first:a,count:s}=te(c),l=a===void 0?null:L(a);if(s===0||l===null||!l.startsWith(`/`))t.push(`${o}: route must be one string starting with /, e.g. route: '/onboarding'. A page has exactly one web address.`);else{let e=r.get(l);e?t.push(`${o}: route '${l}' is already used by ${e}. One page, one file, one address.`):r.set(l,o)}let u=I(c,`template`)[0],d=u===void 0?null:L(u);if(!d||![`board`,`table`,`calendar`,`drive`,`inline_form`,`popup_form`].includes(d)){let r=n===`tab`?m(e):h(e),a=I(c,n===`tab`?`tabName`:`pageName`)[0],s=a===void 0?null:L(a),l=e=>e.toLowerCase().replace(/[^a-z0-9]+/g,`_`).replace(/^_|_$/g,``);if(![r,...s?[l(s),l(s).replace(/_/g,``)]:[]].some(e=>i.has(e))){let n=v(e).replace(/(Tab|Page)$/,``),r=(s||n).split(/[^a-zA-Z0-9]+/).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(``);t.push(`${o}: no content file matches this page, so it renders empty — create src/your-ai-workflow-firebase-os/microcomponents/${r}Content.tsx with export default.`)}}}if(n===`tab`){let e=I(c,`pageType`)[0],n=e===void 0?null:L(e);n!==`user`&&n!==`internal`&&n!==`admin`&&t.push(`${o}: pageType must be 'user', 'internal' or 'admin'. A dashboard page is never public.`)}if(n===`form`){let n=g(e),r=I(c,`systemName`)[0];r!==void 0&&L(r)!==n&&t.push(`${o}: systemName must be '${n}'.`)}continue}if(P(o)){z(c)||t.push(`${o}: add export default.`);let r=_(e).map(e=>a.get(e)).find(Boolean),i=/<form\b/.test(c)||/\bonSubmit\b/.test(c);if(i&&r){let e=H[r];c.includes(e.token)||n.push(`${o}: this page is a ${r} and already has its own add and edit dialog, themed for light and dark — open it with ${e.call} instead of building a form here.`)}else i&&!/ContactPopup/.test(c)&&n.push(`${o}: forms are *Form.config.ts files shown with <ContactPopup formId="…" />; a hand-written form never reaches the Inbox.`);let s=c.match(/['"`](?:mem|user|admin|pub)_notifications['"`]/);s&&t.push(`${o}: ${s[0]} is the bell's own collection and pages never touch it — delete that code and send the notification with notify({ toUid | toRole, title, body, link }) imported from 'your-ai-workflow-firebase-os'.`),/<style[\s>]/.test(c)&&t.push(`${o}: a <style> tag is global CSS and restyles every page of the app — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);let l=c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);l&&n.push(`${o}: ${l[0]} makes the page exactly one screen tall and strands everything below it on a phone — the page is already inside a panel that scrolls, so let it be as long as it needs and drop the height and any overflow-hidden around it.`);let u=c.match(ee);u&&n.push(`${o}: "${u[0]}" is wording off the owner's admin screens — the page is for its visitors, not for the owner or for you. Delete it and any prompt or copy-this box on the page.`);let d=c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);d&&n.push(`${o}: ${d[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`);continue}if(o===d){/<style[\s>]/.test(c)&&t.push(`${o}: a <style> tag is global CSS — while the home page is open it restyles every page of the app, the titles and the menu included — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);let e=c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);e&&n.push(`${o}: ${e[0]} makes the page exactly one screen tall and strands everything below it on a phone — let the page be as long as it needs and drop the height and any overflow-hidden around it.`);let r=c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);r&&n.push(`${o}: ${r[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`)}}return{errors:t,warnings:n}}var ne=[`--bg-color`,`--bg-secondary-color`,`--fg-color`,`--accent-color`,`--panel-bg`,`--panel-border`,`--button-bg`,`--button-fg`,`--button-hover-bg`,`--btn-sec-bg`,`--btn-sec-border`,`--btn-sec-text`,`--text-gradient`,`--glow-color`,`--notification-icon-color`,`--selection-bg`,`--selection-text`],re=(e,t)=>{let n=[],r=t.replace(/\/\*[\s\S]*?\*\//g,``);for(let[,t,i]of r.matchAll(/([^{}]+)\{([^{}]*)\}/g)){let r=t.trim(),a=ne.filter(e=>RegExp(`${e}\\s*:[^;}]*!important`).test(i));if(a.length){n.push(`${e}: \`${a[0]}\` is set with \`!important\` here, and the app writes that colour itself from the owner's theme — forcing it freezes it, so their Theme screen stops working. Delete the \`!important\` and set the colour in configs/theme.config.ts instead, in background, backgroundSecondary, foreground, accent or buttonText; every other colour is derived from those five.`);continue}let o=/(?:^|[\s;])(?:font-size|line-height|font)\s*:/.test(i);if(/\.page-title\b/.test(r)){n.push(`${e}: the rule for \`${r.replace(/\s+/g,` `)}\` styles the app's page title, which is the library's chrome and not this file's to restyle — delete the rule; a page heading you want to size is one you write yourself inside a content file.`);continue}let s=i.match(/(?:^|[\s;])font-family\s*:\s*([^;}]+)/);if(s&&!/^\s*['"]?Inter['"]?\s*(?:,|$)/i.test(s[1])){n.push(`${e}: the rule for \`${r.replace(/\s+/g,` `)}\` sets a \`font-family\`, and the app has one typeface — Inter, everywhere, with no setting for it. Delete the \`font-family\` line; the rest of the rule can stay.`);continue}let c=r.split(`,`).map(e=>e.trim()).filter(e=>/^h[1-3]$/.test(e));c.length&&o&&n.push(`${e}: this rule sets a size on \`${c.join(`, `)}\`, which changes every page title in the app — delete the size (font-weight here is fine) and size the headings you write inside your own content file instead.`)}return n};function W(e,t){return U(e.map(e=>({file:e.file,source:t(e.abs)})))}var G=i,K=`\0`+G,q=[{rel:`src/App.tsx`,source:a},{rel:`src/main.tsx`,source:s}],J=e=>`${e} is owned by Your AI Firebase; your edit was discarded.`,ie=e=>e.replace(/[?#].*$/,``),Y=e=>n.normalize(e).split(n.sep).join(`/`),X={info:e=>console.log(e),warn:e=>console.warn(e),error:e=>console.error(e)};function Z(e,r=X){let i=[];for(let{rel:a,source:o}of q){let s=n.join(e,a),c=null;try{c=t.readFileSync(s,`utf8`)}catch{c=null}c!==o&&(t.mkdirSync(n.dirname(s),{recursive:!0}),t.writeFileSync(s,o),i.push(a),c!==null&&r.warn(J(a)))}return i}function Q(e){let r=n.join(e,u,`registry.ts`);return t.existsSync(r)?!1:(t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,o),!0)}var $=(e,t)=>{let r=Y(ie(t));for(let{rel:t,source:i}of q)if(r===Y(n.join(e,t)))return i;return null};function ae(){let e=process.cwd(),r=!1,i=`build`,a=X,o=()=>{let r=W(C(e),e=>t.readFileSync(e,`utf8`)),i=n.join(e,`src`,`your-ai-workflow-firebase-os`,`theme.css`),a=t.existsSync(i)?re(`src/your-ai-workflow-firebase-os/theme.css`,t.readFileSync(i,`utf8`)):[];return{...r,errors:[...r.errors,...D(e),...a]}};return{name:`your-ai-firebase`,enforce:`pre`,config:()=>({resolve:{dedupe:[`firebase`,`react`,`react-dom`,`react-router-dom`]}}),configResolved(t){e=t.root,i=t.command,a=t.logger,r=x(e),r&&(Z(e,a),Q(e))},resolveId(e){return e===G?K:null},buildStart(){if(!r)return;let{errors:e,warnings:t}=o();for(let e of t)this.warn(e);if(e.length!==0)if(i===`build`)this.error(e[0]);else for(let t of e)a.error(t)},load(t){if(!r)return null;if(t===K){let t=C(e),{errors:n}=o();return n.length&&this.error(n[0]),A(t)}return $(e,t)},configureServer(t){if(!r)return;let i=()=>{let e=t.moduleGraph.getModuleById(K);e&&t.moduleGraph.invalidateModule(e),t.ws.send({type:`full-reload`,path:`*`})},a=r=>{let a=Y(r);if($(e,a)!==null){Z(e,t.config.logger);return}if(!S(e,a)&&!O(a))return;n.relative(e,a).split(n.sep).join(`/`)!==d&&i();let{errors:s,warnings:c}=o();for(let e of c)t.config.logger.warn(e);for(let e of s)t.config.logger.error(e)};t.watcher.on(`add`,a),t.watcher.on(`unlink`,a),t.watcher.on(`change`,a)}}}exports.CANONICAL_APP_TSX=a,exports.CANONICAL_MAIN_TSX=s,exports.CONFIGS_DIR=l,exports.CONSUMER_DIR=c,exports.CONSUMER_REGISTRY_TS=o,exports.MICROCOMPONENTS_DIR=u,exports.OWNED_BANNER=r,exports.RESOLVED_VIRTUAL_ID=K,exports.SHIPPED_CONFIG_FILES=f,exports.VIRTUAL_ID=G,exports.VIRTUAL_REGISTRIES_ID=i,exports.classifyConfigFile=y,exports.discover=C,exports.ensureConsumerRegistry=Q,exports.fileIdFromConfigFile=v,exports.formIdFromFile=g,exports.isConsumerRoot=x,exports.isDiscoveredPath=S,exports.microcomponentKeysFromFile=_,exports.ownedMessage=J,exports.pageIdFromFile=h,exports.registrable=w,exports.restoreOwnedFiles=Z,exports.stripComments=F,exports.tabIdFromFile=m,exports.validate=U,exports.validateDiscovered=W,exports.virtualModuleSource=j,exports.virtualModuleSourceFor=A,exports.yourAiFirebase=ae;
package/dist/vite.es.js CHANGED
@@ -73,89 +73,96 @@ createRoot(document.getElementById('root')!).render(
73
73
  <App />
74
74
  </StrictMode>,
75
75
  )
76
- `, s = "src/your-ai-workflow-firebase-os", c = `${s}/configs`, l = `${s}/microcomponents`, u = [
76
+ `, s = "src/your-ai-workflow-firebase-os", c = `${s}/configs`, l = `${s}/microcomponents`, u = `${s}/Home.tsx`, d = [
77
77
  "theme.config.ts",
78
78
  "contactForm.config.ts",
79
79
  "supportForm.config.ts",
80
80
  "registry.ts",
81
81
  "pagesRegistry.ts",
82
82
  "home.config.ts"
83
- ], d = (e) => e.split(t.sep).join("/"), f = (e) => t.basename(e).replace(".config.ts", "").replace(/Tab$/, "").toLowerCase(), p = (e) => t.basename(e).replace(".config.ts", "").replace(/Page$/, "").toLowerCase(), m = (e) => t.basename(e).replace(".config.ts", "").replace(/Form$/, "").toLowerCase(), h = (e) => {
83
+ ], f = (e) => e.split(t.sep).join("/"), p = (e) => t.basename(e).replace(".config.ts", "").replace(/Tab$/, "").toLowerCase(), m = (e) => t.basename(e).replace(".config.ts", "").replace(/Page$/, "").toLowerCase(), h = (e) => t.basename(e).replace(".config.ts", "").replace(/Form$/, "").toLowerCase(), g = (e) => {
84
84
  let n = t.basename(e).replace(/\.tsx$/, "").replace(/Content$/, "").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
85
85
  return [n, n.replace(/_/g, "")];
86
- }, g = (e) => t.basename(e).replace(/\.config\.ts$/, "");
87
- function _(e) {
86
+ }, _ = (e) => t.basename(e).replace(/\.config\.ts$/, "");
87
+ function v(e) {
88
88
  let n = t.basename(e);
89
- return u.includes(n) ? "shipped" : /Tab\.config\.ts$/.test(n) ? "tab" : /Page\.config\.ts$/.test(n) ? "page" : /Form\.config\.ts$/.test(n) ? "form" : "other";
89
+ return d.includes(n) ? "shipped" : /Tab\.config\.ts$/.test(n) ? "tab" : /Page\.config\.ts$/.test(n) ? "page" : /Form\.config\.ts$/.test(n) ? "form" : "other";
90
90
  }
91
- var v = (t) => e.existsSync(t) ? e.readdirSync(t, { withFileTypes: !0 }).filter((e) => e.isFile()).map((e) => e.name).sort() : [], y = (n) => e.existsSync(t.join(n, s));
92
- function b(e, n) {
93
- let r = d(t.relative(e, n));
94
- return r.startsWith(c + "/") || r.startsWith(l + "/");
91
+ var y = (t) => e.existsSync(t) ? e.readdirSync(t, { withFileTypes: !0 }).filter((e) => e.isFile()).map((e) => e.name).sort() : [], b = (n) => e.existsSync(t.join(n, s));
92
+ function x(e, n) {
93
+ let r = f(t.relative(e, n));
94
+ return r.startsWith(c + "/") || r.startsWith(l + "/") || r === u;
95
95
  }
96
- function x(e) {
97
- let n = [], r = t.join(e, c);
98
- for (let e of v(r)) {
96
+ function S(n) {
97
+ let r = [], i = t.join(n, c);
98
+ for (let e of y(i)) {
99
99
  if (!/\.tsx?$/.test(e)) continue;
100
- let i = t.join(r, e), a = `${c}/${e}`, o = _(e);
101
- o === "tab" ? n.push({
100
+ let n = t.join(i, e), a = `${c}/${e}`, o = v(e);
101
+ o === "tab" ? r.push({
102
102
  kind: o,
103
103
  file: a,
104
- abs: i,
104
+ abs: n,
105
105
  name: e,
106
- id: f(e)
107
- }) : o === "page" ? n.push({
106
+ id: p(e)
107
+ }) : o === "page" ? r.push({
108
108
  kind: o,
109
109
  file: a,
110
- abs: i,
110
+ abs: n,
111
111
  name: e,
112
- id: p(e)
113
- }) : o === "form" || /Form\.config\.ts$/.test(e) ? n.push({
112
+ id: m(e)
113
+ }) : o === "form" || /Form\.config\.ts$/.test(e) ? r.push({
114
114
  kind: "form",
115
115
  file: a,
116
- abs: i,
116
+ abs: n,
117
117
  name: e,
118
- id: m(e)
119
- }) : n.push({
118
+ id: h(e)
119
+ }) : r.push({
120
120
  kind: "other",
121
121
  file: a,
122
- abs: i,
122
+ abs: n,
123
123
  name: e,
124
124
  id: ""
125
125
  });
126
126
  }
127
- let i = t.join(e, l);
128
- for (let e of v(i)) e.endsWith(".tsx") && e.replace(/\.tsx$/, "") !== "registry" && n.push({
127
+ let a = t.join(n, l);
128
+ for (let e of y(a)) e.endsWith(".tsx") && e.replace(/\.tsx$/, "") !== "registry" && r.push({
129
129
  kind: "microcomponent",
130
130
  file: `${l}/${e}`,
131
- abs: t.join(i, e),
131
+ abs: t.join(a, e),
132
132
  name: e,
133
- id: h(e)[0]
133
+ id: g(e)[0]
134
134
  });
135
- return n;
135
+ let o = t.join(n, u);
136
+ return e.existsSync(o) && r.push({
137
+ kind: "other",
138
+ file: u,
139
+ abs: o,
140
+ name: "Home.tsx",
141
+ id: ""
142
+ }), r;
136
143
  }
137
- var S = (e) => e.filter((e) => e.kind !== "other"), C = [{
144
+ var C = (e) => e.filter((e) => e.kind !== "other"), w = [{
138
145
  test: (e) => /Content\.tsx$/.test(e),
139
146
  home: l
140
147
  }, {
141
148
  test: (e) => /(Tab|Page|Form)\.config\.tsx?$/.test(e),
142
149
  home: c
143
- }], w = new Set([
150
+ }], T = new Set([
144
151
  "node_modules",
145
152
  ".git",
146
153
  "dist",
147
154
  "build"
148
155
  ]);
149
- function ee(n) {
156
+ function E(n) {
150
157
  let r = [], i = t.join(n, "src"), a = (i, o) => {
151
158
  if (!(o > 6 || !e.existsSync(i))) for (let s of e.readdirSync(i, { withFileTypes: !0 })) {
152
- let e = t.join(i, s.name), u = d(t.relative(n, e));
159
+ let e = t.join(i, s.name), u = f(t.relative(n, e));
153
160
  if (s.isDirectory()) {
154
- w.has(s.name) || a(e, o + 1);
161
+ T.has(s.name) || a(e, o + 1);
155
162
  continue;
156
163
  }
157
164
  if (!(u === `${c}/${s.name}` || u === `${l}/${s.name}`)) {
158
- for (let { test: e, home: t } of C) if (e(s.name)) {
165
+ for (let { test: e, home: t } of w) if (e(s.name)) {
159
166
  r.push(`${u}: files like this are only found directly in ${t}/ — move it to ${t}/${s.name}.`);
160
167
  break;
161
168
  }
@@ -164,18 +171,18 @@ function ee(n) {
164
171
  };
165
172
  return a(i, 0), r.sort();
166
173
  }
167
- var T = (e) => {
174
+ var ee = (e) => {
168
175
  let n = t.basename(e);
169
- return C.some((e) => e.test(n));
170
- }, E = (e) => JSON.stringify(e);
171
- function D(e) {
176
+ return w.some((e) => e.test(n));
177
+ }, D = (e) => JSON.stringify(e);
178
+ function O(e) {
172
179
  let t = [], n = [], r = [], i = [];
173
- return S(e).forEach((e, a) => {
180
+ return C(e).forEach((e, a) => {
174
181
  let o = `m${a}`;
175
- if (t.push(`import * as ${o} from ${E(d(e.abs))};`), e.kind === "tab") n.push(`{
182
+ if (t.push(`import * as ${o} from ${D(f(e.abs))};`), e.kind === "tab") n.push(`{
176
183
  const configVal = pickConfig(${o});
177
184
  if (configVal) {
178
- const tabId = ${E(e.id)};
185
+ const tabId = ${D(e.id)};
179
186
  const finalTab = {
180
187
  config: {
181
188
  ...configVal,
@@ -194,7 +201,7 @@ function D(e) {
194
201
  else if (e.kind === "page") n.push(`{
195
202
  const configVal = pickConfig(${o});
196
203
  if (configVal) {
197
- const pageId = ${E(e.id)};
204
+ const pageId = ${D(e.id)};
198
205
  const finalPage = {
199
206
  config: {
200
207
  ...configVal,
@@ -213,7 +220,7 @@ function D(e) {
213
220
  else if (e.kind === "form") r.push(`{
214
221
  const configVal = pickConfig(${o});
215
222
  if (configVal) {
216
- const formId = ${E(e.id)};
223
+ const formId = ${D(e.id)};
217
224
  // The form's id is its file name; systemName always follows the file.
218
225
  const finalConfig = { ...configVal, systemName: formId };
219
226
  forms[formId] = finalConfig;
@@ -222,13 +229,13 @@ function D(e) {
222
229
  }
223
230
  }`);
224
231
  else if (e.kind === "microcomponent") {
225
- let [t, n] = h(e.name);
232
+ let [t, n] = g(e.name);
226
233
  i.push(`if (${o} && ${o}.default) {
227
- microcomponents[${E(t)}] = ${o}.default;
234
+ microcomponents[${D(t)}] = ${o}.default;
228
235
  // Also answer to the squashed form ("mobile_apps" -> "mobileapps"): a page id
229
236
  // derived from a file name (mobileAppsPage.config.ts -> "mobileapps") has no
230
237
  // underscores, and the page must still find MobileAppsContent.tsx.
231
- microcomponents[${E(n)}] = ${o}.default;
238
+ microcomponents[${D(n)}] = ${o}.default;
232
239
  }`);
233
240
  }
234
241
  }), `// Generated by Your AI Firebase from the files in ${s}/.
@@ -269,19 +276,19 @@ export const formsRegistry = forms;
269
276
  export const microcomponentRegistry = microcomponents;
270
277
  `;
271
278
  }
272
- var te = (e) => D(x(e)), O = (e) => e.split("/").pop() || e, k = (e) => /\/configs\/[^/]+$/.test(e), A = (e) => /\/microcomponents\/[^/]+\.tsx$/.test(e), j = (e) => e.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:\\'"`])\/\/.*$/gm, "$1"), M = (e, t) => {
279
+ var k = (e) => O(S(e)), A = (e) => e.split("/").pop() || e, j = (e) => /\/configs\/[^/]+$/.test(e), M = (e) => /\/microcomponents\/[^/]+\.tsx$/.test(e), N = (e) => e.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:\\'"`])\/\/.*$/gm, "$1"), P = (e, t) => {
273
280
  let n = RegExp(`(?:^|[\\s,{])${t}\\s*:\\s*([^,\\n}]+)`, "g"), r = [], i;
274
281
  for (; i = n.exec(e);) r.push(i[1].trim());
275
282
  return r;
276
- }, N = (e) => {
283
+ }, F = (e) => {
277
284
  let t = e.match(/^(['"`])([^'"`]*)\1$/);
278
285
  return t ? t[2] : null;
279
- }, P = (e) => /export\s+(?:const|let|var|function|class)\s+\w*Config\b/.test(e) || /export\s*\{[^}]*\b\w*Config\b[^}]*\}/.test(e), F = (e) => /export\s+default\b/.test(e), I = (e) => e.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, ""), L = [
286
+ }, I = (e) => /export\s+(?:const|let|var|function|class)\s+\w*Config\b/.test(e) || /export\s*\{[^}]*\b\w*Config\b[^}]*\}/.test(e), L = (e) => /export\s+default\b/.test(e), R = (e) => e.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, ""), z = [
280
287
  "board",
281
288
  "table",
282
289
  "calendar",
283
290
  "drive"
284
- ], R = {
291
+ ], B = {
285
292
  board: {
286
293
  call: "actions.openInlineEditor(categoryId)",
287
294
  token: "openInlineEditor"
@@ -298,43 +305,43 @@ var te = (e) => D(x(e)), O = (e) => e.split("/").pop() || e, k = (e) => /\/confi
298
305
  call: "the openUploadModal() prop",
299
306
  token: "openUploadModal"
300
307
  }
301
- }, z = /(?:copy (?:this|the sentence|it) (?:in)?to|paste (?:this|it) (?:in)?to|copy for (?:your |the )?(?:ai|assistant)|ask your assistant|your assistant.{0,3}s chat|copy this to change)[^\n]{0,40}/i, B = (e) => {
302
- let t = M(e, "route");
308
+ }, V = /(?:copy (?:this|the sentence|it) (?:in)?to|paste (?:this|it) (?:in)?to|copy for (?:your |the )?(?:ai|assistant)|ask your assistant|your assistant.{0,3}s chat|copy this to change)[^\n]{0,40}/i, te = (e) => {
309
+ let t = P(e, "route");
303
310
  return {
304
311
  first: t[0],
305
312
  count: t.length
306
313
  };
307
314
  };
308
- function V(e) {
315
+ function H(e) {
309
316
  let t = [], n = [], r = /* @__PURE__ */ new Map(), i = /* @__PURE__ */ new Set();
310
- for (let { file: t } of e) if (A(t)) for (let e of h(O(t))) i.add(e);
317
+ for (let { file: t } of e) if (M(t)) for (let e of g(A(t))) i.add(e);
311
318
  let a = /* @__PURE__ */ new Map();
312
319
  for (let { file: t, source: n } of e) {
313
- if (!k(t)) continue;
314
- let e = O(t), r = _(e);
320
+ if (!j(t)) continue;
321
+ let e = A(t), r = v(e);
315
322
  if (r !== "tab" && r !== "page") continue;
316
- let i = j(n), o = M(i, "template")[0], s = o === void 0 ? null : N(o);
317
- if (!s || !L.includes(s)) continue;
318
- let c = M(i, r === "tab" ? "tabName" : "pageName")[0], l = c === void 0 ? null : N(c), u = [r === "tab" ? f(e) : p(e), ...l ? [I(l), I(l).replace(/_/g, "")] : []];
323
+ let i = N(n), o = P(i, "template")[0], s = o === void 0 ? null : F(o);
324
+ if (!s || !z.includes(s)) continue;
325
+ let c = P(i, r === "tab" ? "tabName" : "pageName")[0], l = c === void 0 ? null : F(c), u = [r === "tab" ? p(e) : m(e), ...l ? [R(l), R(l).replace(/_/g, "")] : []];
319
326
  for (let e of u) a.set(e, s);
320
327
  }
321
328
  for (let { file: o, source: s } of e) {
322
- let e = O(o), c = j(s);
323
- if (k(o)) {
324
- let n = _(e);
329
+ let e = A(o), c = N(s);
330
+ if (j(o)) {
331
+ let n = v(e);
325
332
  if (n === "shipped") continue;
326
333
  if (n === "other") {
327
334
  t.push(`${o}: rename it to somethingTab.config.ts (a dashboard page), somethingPage.config.ts (a page for anyone) or somethingForm.config.ts (a form).`);
328
335
  continue;
329
336
  }
330
- if (!P(c) && !F(c) && t.push(`${o}: export one const named ${g(e)}Config.`), n === "tab" || n === "page") {
331
- let { first: a, count: s } = B(c), l = a === void 0 ? null : N(a);
337
+ if (!I(c) && !L(c) && t.push(`${o}: export one const named ${_(e)}Config.`), n === "tab" || n === "page") {
338
+ let { first: a, count: s } = te(c), l = a === void 0 ? null : F(a);
332
339
  if (s === 0 || l === null || !l.startsWith("/")) t.push(`${o}: route must be one string starting with /, e.g. route: '/onboarding'. A page has exactly one web address.`);
333
340
  else {
334
341
  let e = r.get(l);
335
342
  e ? t.push(`${o}: route '${l}' is already used by ${e}. One page, one file, one address.`) : r.set(l, o);
336
343
  }
337
- let u = M(c, "template")[0], d = u === void 0 ? null : N(u);
344
+ let u = P(c, "template")[0], d = u === void 0 ? null : F(u);
338
345
  if (!d || ![
339
346
  "board",
340
347
  "table",
@@ -343,37 +350,46 @@ function V(e) {
343
350
  "inline_form",
344
351
  "popup_form"
345
352
  ].includes(d)) {
346
- let r = n === "tab" ? f(e) : p(e), a = M(c, n === "tab" ? "tabName" : "pageName")[0], s = a === void 0 ? null : N(a), l = (e) => e.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
353
+ let r = n === "tab" ? p(e) : m(e), a = P(c, n === "tab" ? "tabName" : "pageName")[0], s = a === void 0 ? null : F(a), l = (e) => e.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
347
354
  if (![r, ...s ? [l(s), l(s).replace(/_/g, "")] : []].some((e) => i.has(e))) {
348
- let n = g(e).replace(/(Tab|Page)$/, ""), r = (s || n).split(/[^a-zA-Z0-9]+/).filter(Boolean).map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join("");
355
+ let n = _(e).replace(/(Tab|Page)$/, ""), r = (s || n).split(/[^a-zA-Z0-9]+/).filter(Boolean).map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join("");
349
356
  t.push(`${o}: no content file matches this page, so it renders empty — create src/your-ai-workflow-firebase-os/microcomponents/${r}Content.tsx with export default.`);
350
357
  }
351
358
  }
352
359
  }
353
360
  if (n === "tab") {
354
- let e = M(c, "pageType")[0], n = e === void 0 ? null : N(e);
361
+ let e = P(c, "pageType")[0], n = e === void 0 ? null : F(e);
355
362
  n !== "user" && n !== "internal" && n !== "admin" && t.push(`${o}: pageType must be 'user', 'internal' or 'admin'. A dashboard page is never public.`);
356
363
  }
357
364
  if (n === "form") {
358
- let n = m(e), r = M(c, "systemName")[0];
359
- r !== void 0 && N(r) !== n && t.push(`${o}: systemName must be '${n}'.`);
365
+ let n = h(e), r = P(c, "systemName")[0];
366
+ r !== void 0 && F(r) !== n && t.push(`${o}: systemName must be '${n}'.`);
360
367
  }
361
368
  continue;
362
369
  }
363
- if (A(o)) {
364
- F(c) || t.push(`${o}: add export default.`);
365
- let r = h(e).map((e) => a.get(e)).find(Boolean), i = /<form\b/.test(c) || /\bonSubmit\b/.test(c);
370
+ if (M(o)) {
371
+ L(c) || t.push(`${o}: add export default.`);
372
+ let r = g(e).map((e) => a.get(e)).find(Boolean), i = /<form\b/.test(c) || /\bonSubmit\b/.test(c);
366
373
  if (i && r) {
367
- let e = R[r];
374
+ let e = B[r];
368
375
  c.includes(e.token) || n.push(`${o}: this page is a ${r} and already has its own add and edit dialog, themed for light and dark — open it with ${e.call} instead of building a form here.`);
369
376
  } else i && !/ContactPopup/.test(c) && n.push(`${o}: forms are *Form.config.ts files shown with <ContactPopup formId="…" />; a hand-written form never reaches the Inbox.`);
370
- /<style[\s>]/.test(c) && t.push(`${o}: a <style> tag is global CSS and restyles every page of the app — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);
371
- let s = c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);
372
- s && n.push(`${o}: ${s[0]} makes the page exactly one screen tall and strands everything below it on a phone — the page is already inside a panel that scrolls, so let it be as long as it needs and drop the height and any overflow-hidden around it.`);
373
- let l = c.match(z);
374
- l && n.push(`${o}: "${l[0]}" is wording off the owner's admin screens — the page is for its visitors, not for the owner or for you. Delete it and any prompt or copy-this box on the page.`);
375
- let u = c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);
376
- u && n.push(`${o}: ${u[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`);
377
+ let s = c.match(/['"`](?:mem|user|admin|pub)_notifications['"`]/);
378
+ s && t.push(`${o}: ${s[0]} is the bell's own collection and pages never touch it — delete that code and send the notification with notify({ toUid | toRole, title, body, link }) imported from 'your-ai-workflow-firebase-os'.`), /<style[\s>]/.test(c) && t.push(`${o}: a <style> tag is global CSS and restyles every page of the app — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);
379
+ let l = c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);
380
+ l && n.push(`${o}: ${l[0]} makes the page exactly one screen tall and strands everything below it on a phone — the page is already inside a panel that scrolls, so let it be as long as it needs and drop the height and any overflow-hidden around it.`);
381
+ let u = c.match(V);
382
+ u && n.push(`${o}: "${u[0]}" is wording off the owner's admin screens — the page is for its visitors, not for the owner or for you. Delete it and any prompt or copy-this box on the page.`);
383
+ let d = c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);
384
+ d && n.push(`${o}: ${d[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`);
385
+ continue;
386
+ }
387
+ if (o === u) {
388
+ /<style[\s>]/.test(c) && t.push(`${o}: a <style> tag is global CSS — while the home page is open it restyles every page of the app, the titles and the menu included — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);
389
+ let e = c.match(/(?<![\w-])(?:h|max-h)-(?:screen|dvh|svh|lvh|\[(?:100|9\d|8\d)(?:vh|dvh|svh|lvh)\])/);
390
+ e && n.push(`${o}: ${e[0]} makes the page exactly one screen tall and strands everything below it on a phone — let the page be as long as it needs and drop the height and any overflow-hidden around it.`);
391
+ let r = c.match(/\b(?:bg-(?:white|black)|(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d+)\b/);
392
+ r && n.push(`${o}: ${r[0]} is a hard-coded colour that ignores the owner's theme and stays wrong in light or dark mode — use bg-background, text-foreground, border-[var(--panel-border)], glass-panel or btn-primary.`);
377
393
  }
378
394
  }
379
395
  return {
@@ -381,7 +397,7 @@ function V(e) {
381
397
  warnings: n
382
398
  };
383
399
  }
384
- var H = [
400
+ var ne = [
385
401
  "--bg-color",
386
402
  "--bg-secondary-color",
387
403
  "--fg-color",
@@ -399,10 +415,10 @@ var H = [
399
415
  "--notification-icon-color",
400
416
  "--selection-bg",
401
417
  "--selection-text"
402
- ], U = (e, t) => {
418
+ ], re = (e, t) => {
403
419
  let n = [], r = t.replace(/\/\*[\s\S]*?\*\//g, "");
404
420
  for (let [, t, i] of r.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
405
- let r = t.trim(), a = H.filter((e) => RegExp(`${e}\\s*:[^;}]*!important`).test(i));
421
+ let r = t.trim(), a = ne.filter((e) => RegExp(`${e}\\s*:[^;}]*!important`).test(i));
406
422
  if (a.length) {
407
423
  n.push(`${e}: \`${a[0]}\` is set with \`!important\` here, and the app writes that colour itself from the owner's theme — forcing it freezes it, so their Theme screen stops working. Delete the \`!important\` and set the colour in configs/theme.config.ts instead, in background, backgroundSecondary, foreground, accent or buttonText; every other colour is derived from those five.`);
408
424
  continue;
@@ -422,35 +438,35 @@ var H = [
422
438
  }
423
439
  return n;
424
440
  };
425
- function W(e, t) {
426
- return V(e.map((e) => ({
441
+ function U(e, t) {
442
+ return H(e.map((e) => ({
427
443
  file: e.file,
428
444
  source: t(e.abs)
429
445
  })));
430
446
  }
431
447
  //#endregion
432
448
  //#region src/vite/plugin.ts
433
- var G = r, K = "\0" + G, q = [{
449
+ var W = r, G = "\0" + W, K = [{
434
450
  rel: "src/App.tsx",
435
451
  source: i
436
452
  }, {
437
453
  rel: "src/main.tsx",
438
454
  source: o
439
- }], J = (e) => `${e} is owned by Your AI Firebase; your edit was discarded.`, ne = (e) => e.replace(/[?#].*$/, ""), Y = (e) => t.normalize(e).split(t.sep).join("/"), X = {
455
+ }], q = (e) => `${e} is owned by Your AI Firebase; your edit was discarded.`, J = (e) => e.replace(/[?#].*$/, ""), Y = (e) => t.normalize(e).split(t.sep).join("/"), X = {
440
456
  info: (e) => console.log(e),
441
457
  warn: (e) => console.warn(e),
442
458
  error: (e) => console.error(e)
443
459
  };
444
460
  function Z(n, r = X) {
445
461
  let i = [];
446
- for (let { rel: a, source: o } of q) {
462
+ for (let { rel: a, source: o } of K) {
447
463
  let s = t.join(n, a), c = null;
448
464
  try {
449
465
  c = e.readFileSync(s, "utf8");
450
466
  } catch {
451
467
  c = null;
452
468
  }
453
- c !== o && (e.mkdirSync(t.dirname(s), { recursive: !0 }), e.writeFileSync(s, o), i.push(a), c !== null && r.warn(J(a)));
469
+ c !== o && (e.mkdirSync(t.dirname(s), { recursive: !0 }), e.writeFileSync(s, o), i.push(a), c !== null && r.warn(q(a)));
454
470
  }
455
471
  return i;
456
472
  }
@@ -459,18 +475,18 @@ function Q(n) {
459
475
  return e.existsSync(r) ? !1 : (e.mkdirSync(t.dirname(r), { recursive: !0 }), e.writeFileSync(r, a), !0);
460
476
  }
461
477
  var $ = (e, n) => {
462
- let r = Y(ne(n));
463
- for (let { rel: n, source: i } of q) if (r === Y(t.join(e, n))) return i;
478
+ let r = Y(J(n));
479
+ for (let { rel: n, source: i } of K) if (r === Y(t.join(e, n))) return i;
464
480
  return null;
465
481
  };
466
- function re() {
482
+ function ie() {
467
483
  let n = process.cwd(), r = !1, i = "build", a = X, o = () => {
468
- let r = W(x(n), (t) => e.readFileSync(t, "utf8")), i = t.join(n, "src", "your-ai-workflow-firebase-os", "theme.css"), a = e.existsSync(i) ? U("src/your-ai-workflow-firebase-os/theme.css", e.readFileSync(i, "utf8")) : [];
484
+ let r = U(S(n), (t) => e.readFileSync(t, "utf8")), i = t.join(n, "src", "your-ai-workflow-firebase-os", "theme.css"), a = e.existsSync(i) ? re("src/your-ai-workflow-firebase-os/theme.css", e.readFileSync(i, "utf8")) : [];
469
485
  return {
470
486
  ...r,
471
487
  errors: [
472
488
  ...r.errors,
473
- ...ee(n),
489
+ ...E(n),
474
490
  ...a
475
491
  ]
476
492
  };
@@ -485,10 +501,10 @@ function re() {
485
501
  "react-router-dom"
486
502
  ] } }),
487
503
  configResolved(e) {
488
- n = e.root, i = e.command, a = e.logger, r = y(n), r && (Z(n, a), Q(n));
504
+ n = e.root, i = e.command, a = e.logger, r = b(n), r && (Z(n, a), Q(n));
489
505
  },
490
506
  resolveId(e) {
491
- return e === G ? K : null;
507
+ return e === W ? G : null;
492
508
  },
493
509
  buildStart() {
494
510
  if (!r) return;
@@ -499,35 +515,35 @@ function re() {
499
515
  },
500
516
  load(e) {
501
517
  if (!r) return null;
502
- if (e === K) {
503
- let e = x(n), { errors: t } = o();
504
- return t.length && this.error(t[0]), D(e);
518
+ if (e === G) {
519
+ let e = S(n), { errors: t } = o();
520
+ return t.length && this.error(t[0]), O(e);
505
521
  }
506
522
  return $(n, e);
507
523
  },
508
524
  configureServer(e) {
509
525
  if (!r) return;
510
- let t = () => {
511
- let t = e.moduleGraph.getModuleById(K);
526
+ let i = () => {
527
+ let t = e.moduleGraph.getModuleById(G);
512
528
  t && e.moduleGraph.invalidateModule(t), e.ws.send({
513
529
  type: "full-reload",
514
530
  path: "*"
515
531
  });
516
- }, i = (r) => {
517
- let i = Y(r);
518
- if ($(n, i) !== null) {
532
+ }, a = (r) => {
533
+ let a = Y(r);
534
+ if ($(n, a) !== null) {
519
535
  Z(n, e.config.logger);
520
536
  return;
521
537
  }
522
- if (!b(n, i) && !T(i)) return;
523
- t();
524
- let { errors: a, warnings: s } = o();
525
- for (let t of s) e.config.logger.warn(t);
526
- for (let t of a) e.config.logger.error(t);
538
+ if (!x(n, a) && !ee(a)) return;
539
+ t.relative(n, a).split(t.sep).join("/") !== u && i();
540
+ let { errors: s, warnings: c } = o();
541
+ for (let t of c) e.config.logger.warn(t);
542
+ for (let t of s) e.config.logger.error(t);
527
543
  };
528
- e.watcher.on("add", i), e.watcher.on("unlink", i), e.watcher.on("change", i);
544
+ e.watcher.on("add", a), e.watcher.on("unlink", a), e.watcher.on("change", a);
529
545
  }
530
546
  };
531
547
  }
532
548
  //#endregion
533
- export { i as CANONICAL_APP_TSX, o as CANONICAL_MAIN_TSX, c as CONFIGS_DIR, s as CONSUMER_DIR, a as CONSUMER_REGISTRY_TS, l as MICROCOMPONENTS_DIR, n as OWNED_BANNER, K as RESOLVED_VIRTUAL_ID, u as SHIPPED_CONFIG_FILES, G as VIRTUAL_ID, r as VIRTUAL_REGISTRIES_ID, _ as classifyConfigFile, x as discover, Q as ensureConsumerRegistry, g as fileIdFromConfigFile, m as formIdFromFile, y as isConsumerRoot, b as isDiscoveredPath, h as microcomponentKeysFromFile, J as ownedMessage, p as pageIdFromFile, S as registrable, Z as restoreOwnedFiles, j as stripComments, f as tabIdFromFile, V as validate, W as validateDiscovered, te as virtualModuleSource, D as virtualModuleSourceFor, re as yourAiFirebase };
549
+ export { i as CANONICAL_APP_TSX, o as CANONICAL_MAIN_TSX, c as CONFIGS_DIR, s as CONSUMER_DIR, a as CONSUMER_REGISTRY_TS, l as MICROCOMPONENTS_DIR, n as OWNED_BANNER, G as RESOLVED_VIRTUAL_ID, d as SHIPPED_CONFIG_FILES, W as VIRTUAL_ID, r as VIRTUAL_REGISTRIES_ID, v as classifyConfigFile, S as discover, Q as ensureConsumerRegistry, _ as fileIdFromConfigFile, h as formIdFromFile, b as isConsumerRoot, x as isDiscoveredPath, g as microcomponentKeysFromFile, q as ownedMessage, m as pageIdFromFile, C as registrable, Z as restoreOwnedFiles, N as stripComments, p as tabIdFromFile, H as validate, U as validateDiscovered, k as virtualModuleSource, O as virtualModuleSourceFor, ie as yourAiFirebase };