diff --git a/.agents/skills/spec-impl/SKILL.md b/.agents/skills/spec-impl/SKILL.md new file mode 100644 index 0000000..d15ae3c --- /dev/null +++ b/.agents/skills/spec-impl/SKILL.md @@ -0,0 +1,235 @@ +--- +name: spec-impl +description: Implements an approved spec. Validates that the state means "Approved" (in any language), creates a git branch named after the spec, switches to it, and starts the implementation step by step with pauses to review diffs. +disable-model-invocation: true +argument-hint: +allowed-tools: Read, Glob, Grep, Edit, Write, AskUserQuestion, Bash(git status:*), Bash(git branch:*), Bash(git checkout:*), Bash(git log:*), Bash(git diff:*), Bash(git stash:*), Bash(cat:*), Bash(ls:*) +--- + +# /spec-impl — Implementer of approved specs + +## Session context + +Current repository state: +!`git status --short` + +Current branch: +!`git branch --show-current` + +Specs available in this folder: +!`ls specs/ 2>/dev/null || echo "The specs/ folder does not exist"` + +Branch-creation config: +!`cat specs/.spec-config.yml 2>/dev/null || echo "AutoCreateBranch: true (default, no config file)"` + +--- + +## Instructions + +Follow these four phases in strict order. **Do not advance to the next phase if the previous one did not complete correctly.** + +--- + +### Phase 1 — Identify the spec + +The received argument is: `$ARGUMENTS` + +If `$ARGUMENTS` is empty: + +- List the files available in `specs/` (you already have them above). +- Ask the user to specify the exact name of the spec. +- Stop and wait for an answer. Do not continue. + +If `$ARGUMENTS` has a value: + +- Look for the file in `specs/`. The user may have written the full name (`01-mvp-arkanoid`), only the number (`01`), or only the slug (`mvp-arkanoid`). Try to find the correct file in any of those cases. +- If you do not find the file, show the available specs and ask the user to correct the name. +- If you do find it, continue to Phase 2. + +--- + +### Phase 2 — Validate the spec's state + +Read the spec file you located in Phase 1 using the Read tool or `cat`. + +In the file's contents, look for the line that contains the spec's state. The header label is typically `**Status:**` (English) or `**Estado:**` (Spanish), but it may use any language. Match by position (status line near the top of the spec) and by the surrounding state machine, not by the exact label. + +**Absolute rule:** You can only continue if the state **means "Approved"** — regardless of the language used. + +Treat any of the following (and their equivalents in other languages) as the **Approved** state and continue: + +- English: `Approved` +- Spanish: `Aprobado` +- Portuguese: `Aprovado` +- French: `Approuvé` +- German: `Genehmigt` +- Italian: `Approvato` +- …or any other language's word that clearly means "approved" + +Anything else (Draft / Borrador, In review / En revisión, Implemented / Implementado, Obsolete / Obsoleto, or any unrecognized value) means **stop** and show the error message below. + +| State category | Examples (any language) | Action | +| ----------------------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------- | +| Approved | `Approved`, `Aprobado`, `Aprovado`, `Approuvé`, … | Continue to Phase 3. | +| Draft | `Draft`, `Borrador`, … | Stop. Show the error message below. | +| In review | `In review`, `En revisión`, … | Stop. Show the error message below. | +| Implemented | `Implemented`, `Implementado`, … | Stop. Show the error message below. | +| Obsolete | `Obsolete`, `Obsoleto`, … | Stop. Show the error message below. | +| State line not found / unrecognized value | — | Stop. The file does not follow the expected format. Tell this to the user. | + +If you are unsure whether a value means "approved", **do not assume**. Stop and ask the user to clarify or to update the spec to the canonical wording. + +**Standard error message when the state does not mean Approved:** + +``` +❌ I cannot implement this spec. + +Current state: [STATE FOUND] +I only work with specs whose state means "Approved" (e.g. `Approved`, `Aprobado`, +or the equivalent in another language). + +To continue you have two options: + 1. If the spec is ready to be implemented, open it and change the state + to "Approved" (or the equivalent term your team uses) manually. + That change is made by the human, not the agent. + 2. If the spec still needs work, use /spec [name] to resume it. +``` + +Do not offer alternatives, do not suggest "I can still start if you want". The block is intentional. + +--- + +### Phase 3 — Create the git branch and switch to it + +Once you have confirmed the state means `Approved`: + +0. **Check the working tree first.** Look at the `git status --short` output in the session context above. If it is **not empty**, stop and show the pending changes, then ask: + + ``` + ⚠️ There are uncommitted changes in the working tree. + Switching branches would carry them over. What do you want to do? + 1. Commit or stash them yourself, then re-run this command (recommended) + 2. Continue anyway — the changes travel to the new branch + ``` + + Wait for the answer. **Do not stash or commit on the user's behalf** unless they explicitly ask for it. If the working tree is clean, skip straight to step 1 without mentioning it. + +1. Derive the branch name from the spec file's full name, without the extension. Format: `spec-NN-slug`. Examples: + + - `01-mvp-arkanoid.md` → branch `spec-01-mvp-arkanoid` + - `02-powerups.md` → branch `spec-02-powerups` + +2. Read the `AutoCreateBranch` flag from the **Branch-creation config** shown in the session context above. + + - If the config file does not exist, the value is missing, or the value is unrecognized → treat it as `true` (the default). + - Only an explicit `false` (in any capitalization) disables automatic branch creation. + + **If `AutoCreateBranch` is `true` (default):** proceed without asking. + + - If the branch **does not exist**: create it with `git checkout -b spec-NN-slug`. + - If it **already exists**: this means previous work is being resumed. Switch to it, read `git log --oneline` on the branch, and tell the user which steps of the plan already look done and which step you propose to resume from. Wait for confirmation on the resume point before implementing anything. + - In both cases: switch to the branch with `git checkout spec-NN-slug` and confirm the change was successful before continuing. + + **If `AutoCreateBranch` is `false`:** ask before touching git. Show: + + ``` + AutoCreateBranch is set to false. + Create and switch to the branch spec-NN-slug? [y/N] + ``` + + - If the user answers **yes**: create/switch to the branch exactly as in the `true` case above. + - If the user answers **no** or leaves it empty: **do not create any branch.** Tell the user you will implement on the current branch (the one shown in the session context above) and ask for explicit confirmation to continue there. Do not improvise — wait for the answer. + +3. Visually confirm to the user the spec is ready and which branch is active: + + ``` + ✅ Ready to implement. + + Spec: specs/NN-slug.md + Branch: spec-NN-slug (active) (← or the current branch, if no new branch was created) + State: Approved (← echo back the actual value found in the spec) + ``` + +4. **Do not start implementing yet.** First show the spec summary to the user so they have it fresh. Extract and show: + - The **objective** (the line after `**Objective:**` / `**Objetivo:**` / equivalent label). + - The **scope** (the `## Scope` / `## Alcance` / equivalent section). + - The **implementation plan** (the section with the numbered steps — `## Implementation plan` / `## Plan de implementación` / equivalent). + - The **acceptance criteria** (the checklist — `## Acceptance criteria` / `## Criterios de aceptación` / equivalent). + +Match section headings by meaning, not by exact wording — the spec may be authored in any language. + +--- + +### Phase 4 — Implement step by step + +After showing the spec summary, tell the user: + +``` +I am going to implement the spec following the implementation plan exactly. +I will pause after each step so you can review the diff. + +Shall we start with Step 1? +``` + +Wait for explicit confirmation ("yes", "go ahead", "go", or equivalent). Do not start without it. + +Once confirmed, follow these rules during the entire implementation: + +**Never commit automatically.** Not per step, not at the end. You write the code and show the diff; committing is the user's decision and the user's command. Only commit if they explicitly ask you to. + +**One rule above all:** implement what the spec says. If something in the spec looks suboptimal to you, mention it as an observation but implement what was agreed. Changes to the spec go into the spec, not into the code by surprise. + +**Work rhythm:** + +- Implement one step of the plan. +- Show a summary of which files you touched and what you did. +- Say: `Step N completed. Could you review the diff and let me know if I continue with Step N+1?` +- Wait for confirmation before continuing. + +**If during the implementation you find an ambiguity** the spec does not resolve: + +- Stop. +- Describe the ambiguity exactly. +- Present two or three concrete options. +- Wait for the user's decision. +- Do not improvise. + +**If the user asks for something that is out of the spec's scope:** + +- Remind them that it is out of this spec's scope. +- Suggest noting it down for the next spec. +- Do not implement it on this branch. + +**When finishing the last step:** + +``` +✅ All steps of the plan are implemented. + +Next step: verify the spec's acceptance criteria one by one. +If they all pass, update the spec's state to "Implemented" (or the equivalent +in your repo's language) and make the final commit before merging this branch. +``` + +--- + +## Summary of expected behavior + +``` +/spec-impl 01-mvp-arkanoid + + Phase 1 → Finds specs/01-mvp-arkanoid.md + Phase 2 → Reads the state → "Approved" (or "Aprobado", etc.) → ✅ continues + Phase 3 → git checkout -b spec-01-mvp-arkanoid → git checkout spec-01-mvp-arkanoid + Shows objective, scope, plan and criteria + Phase 4 → Implements step by step with pauses + Ends by reminding to verify the acceptance criteria + +/spec-impl 02-powerups (state: Draft / Borrador) + + Phase 1 → Finds specs/02-powerups.md + Phase 2 → Reads the state → "Draft" → ❌ stops + Shows the standard error message + Does not create branch, does not touch code +``` + +**Branch creation is controlled by the `AutoCreateBranch` flag** in `specs/.spec-config.yml`. It defaults to `true` (create the branch automatically, as shown above). Set it to `false` to make Phase 3 ask `[y/N]` before creating the branch. diff --git a/.agents/skills/spec/SKILL.md b/.agents/skills/spec/SKILL.md new file mode 100644 index 0000000..6be6c97 --- /dev/null +++ b/.agents/skills/spec/SKILL.md @@ -0,0 +1,164 @@ +--- +name: spec +description: Designs and develops specs following the spec-driven method. Asks clarifying questions before proposing structure, and builds the spec section by section. Use it when starting a large feature, before writing code. +disable-model-invocation: true +argument-hint: 'short feature description or requirement' +allowed-tools: Read, Glob, Grep, Write, AskUserQuestion, Bash(ls:*), Bash(cat:*), Bash(date:*) +--- + +# /spec — Guided spec designer + +## Session context + +Today's date (use this for the spec header, never guess it): +!`date +%F` + +Specs that already exist: +!`ls specs/ 2>/dev/null || echo "The specs/ folder does not exist yet"` + +--- + +This skill helps you produce a useful spec following the spec-driven method. **You don't write code here.** Your job is to help the user clarify what they want to build, ask questions when something is not well-defined enough, and develop the spec section by section until it is ready to be saved into `specs/`. + +## Philosophy + +A spec is not decorative documentation. It is the contract that drives later execution. If the spec is vague, the code will improvise. That is why this flow is **deliberately slow during the definition phase** and **fast during the writing phase**. + +Read `template.md` (in the same directory as this skill) to see the full structure the spec will follow. Lean on it at every step. + +## Command flow + +- Follow the four phases in order. **Never skip Phase 2** — the questions are the whole point. If the user wants to go faster, remind them that the cost of a bad spec gets paid later in code. (Phase 3 does have a fast path once Phase 2 is genuinely complete; see below.) +- Your replies must be in the same language as the initial prompt. E.g.: if the initial prompt is in Spanish, your replies must be in Spanish; if it is in English, your replies must be in English. + +### Phase 1 — Understand the context + +Before asking questions about the feature, make sure you have project context: + +1. Read the project-memory file, if one exists. Try in order and stop at the first hit: `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `README.md`. This adapts the skill to whichever agent is running it (Claude Code, Codex, Gemini CLI, etc.). +2. Look at the `specs/` listing in the session context above to see which specs already exist and how they are numbered. +3. If previous specs exist, read at least the two most recent ones to pick up the project's conventions — including the **language** they are written in and the exact wording they use for states and section headings. A new spec must match the existing ones. + +If the `$ARGUMENTS` argument comes in empty, ask the user for an initial **single-sentence** description of what they want to build. If the description does not fit in one sentence, that is the first signal that the feature is too big — suggest splitting it before continuing. + +### Phase 2 — Clarify through questions + +This is the most important phase of the command. Your job here is to **detect ambiguities and ask**, not to assume. + +Ask questions in blocks of 3 to 5 at a time (not one single question followed by another single question — that is exhausting). After each block, wait for an answer before continuing. + +**Question categories you should always consider:** + +- **Scope:** What is in and what is NOT? Which parts of the feature are deferred to another spec? +- **Data:** What new structures are introduced? How are they named? Where do they live? +- **Integration:** Does this feature depend on previous specs? Does it modify something existing or only add? +- **Persistence:** Is anything saved between sessions? Where? With what versioning? +- **UX and states:** What does it look like when it works? What does it look like when it fails? Are there intermediate states? +- **Risks:** What can break this? What happens in the degraded case? +- **Closed decisions:** Is there any decision the user has already made and does not want to reopen? + +**How to phrase the questions:** + +- Use concrete questions, not open-ended ones. ❌ "How do you imagine persistence?" → ✅ "Is persistence localStorage, IndexedDB, or a JSON file on disk?" +- When you offer options, give 2–4, mark which one is your recommendation and why. +- If your agent exposes a native multiple-choice question tool (in Claude Code: `AskUserQuestion`), use it for these blocks instead of writing the options as prose — the user picks instead of typing. Put your recommendation first and label it. Fall back to a numbered markdown list when no such tool exists. +- If you spot an answer that would open Pandora's box (e.g. "and we also want multiplayer"), point out that it deserves its own spec and ask whether we leave it out of this one's scope. + +**When to stop asking:** + +Stop when you can answer these three questions without assuming anything: + +1. Which files will appear or change? +2. What is the first executable step and what is the last one? +3. How do I verify the feature is finished? + +If you still cannot answer one of them, keep asking. + +### Phase 3 — Write the spec + +Once Phase 2 is closed, decide how to write it: + +**If you already have all the information you need** — meaning you can answer the three Phase 2 questions (which files change, what the first and last executable steps are, how to verify it is finished) **without assuming anything** — then **do not go section by section**. Write the complete spec and jump straight to Phase 4 to save the file. Do not ask for section-by-section confirmation, and do not show a draft for approval first: the user already answered everything in Phase 2, and re-asking is friction. The user reviews the saved file and asks for changes if needed. + +**Only if information is still missing** (the user cut Phase 2 short, an answer was vague, or some section cannot be written without inventing something), develop the sections **one by one**, showing each one and waiting for confirmation before moving to the next. + +In both cases the content follows the same order: + +1. **Header** (state, dependencies, date, one-sentence objective). The one-sentence objective is critical — if it does not fit in one sentence, go back to Phase 2. +2. **Scope** (what is in and what is NOT). The "not in" must be explicit. +3. **Data model** (concrete structures with real names). If the feature introduces no new data, skip this section and say so explicitly. +4. **Implementation plan** (numbered steps, each leaving the system functional). +5. **Acceptance criteria** (boolean checklist, not aspirational). +6. **Decisions taken and discarded** (with brief justification). +7. **Identified risks** (only if applicable — if no relevant risks exist, skip it). + +**After each section (only in the section-by-section mode):** + +- Show it formatted in markdown. +- Ask: "Does this section stay like this or do you want to tweak it?" +- If the user requests changes, apply them and show again. +- Only move to the next section once the user confirms. + +**Common mistakes to avoid:** + +- Generating acceptance criteria that are not verifiable ("that it works well"). +- Putting things into the implementation plan that are not in the scope. +- Assuming file names or structures the user did not confirm. +- Skipping the decisions section — that section is the one with the most long-term value. + +### Phase 4 — Save the spec + +When the content is ready (either because you had everything, or because all sections were confirmed): + +1. Determine the next sequential number from the `specs/` listing in the session context. Take the highest existing number and add one, zero-padded to two digits. If the last one is `02-powerups.md`, this one will be `03-`. If `specs/` is empty or missing, start at `01-`. +2. Generate a short kebab-case slug from the objective (e.g. `levels-and-highscores`). See **Arguments** below for when `$ARGUMENTS` is the slug instead. +3. Use the date from the session context above for the `**Date:**` field. **Never write a date you did not read from there.** +4. Write the file directly at `specs/NN-slug.md` with all the sections. **Do not ask for permission to write it and do not ask whether the file name works** — announce the path in the final confirmation. Only ask if the target file already exists. +5. Mark the state as `Draft` by default (or the equivalent word used by the existing specs in this repo). **Do not mark it as `Approved` automatically** — the user does that once they have re-read it. +6. If the header lists dependencies (`**Depends on:** SPEC 01`), check that each referenced spec actually exists in `specs/`. If one does not, say so instead of writing a dangling reference. +7. **Seed the config file if it does not exist.** Check for `specs/.spec-config.yml`. If it is **missing**, create it with the default content below. If it **already exists, leave it untouched** — never overwrite the user's settings. + + ```yaml + # spec workflow configuration + # + # AutoCreateBranch — controls whether /spec-impl creates the git branch automatically. + # true (default) → /spec-impl creates and switches to spec-NN-slug without asking + # false → /spec-impl asks for [y/N] confirmation before creating the branch + AutoCreateBranch: true + ``` + +8. Confirm to the user: + - Path of the created file. + - Reminder: the spec is in `Draft` state. Change it to `Approved` once you have re-read it. + - If you just created `specs/.spec-config.yml`, mention it exists and that `AutoCreateBranch` defaults to `true` (set it to `false` to control branch creation yourself). + - Next step: once reviewed and approved, run `/spec-impl NN-slug` to implement it. + - **Stop here.** Do not propose implementing the spec, writing code, or taking any further action beyond this confirmation. + +## Hard rules + +- **Never write code during this command.** Only the spec's `.md` file at the end. +- **Never propose implementing the spec after saving it.** Your job ends when the file is written. The user runs `/spec-impl` when they are ready. +- **Never assume decisions the user did not confirm.** If you are missing information, ask — in Phase 2, which is where the questions belong. +- **Do not re-ask in Phase 3 what was already answered in Phase 2.** If the information is complete, write the whole spec and save it. Section-by-section confirmation is the fallback for incomplete information, not the default. +- **If the user wants to speed up and skip Phase 2**, remind them: "Questions now save hours later. Are you sure you want to skip them?". If they insist, respect their decision but record it in the spec's decisions section ("Quick definition without detailed clarification"). +- **If the feature is too big** (does not fit in one sentence, touches more than three areas of the system, requires decisions in four or more domains), propose splitting it into two or more specs before continuing. + +## Tone when asking questions + +Be direct and specific. Do not apologize for asking. Do not use phrases like "if you don't mind..." or "could you maybe...". The user invoked this skill precisely because they want you to ask questions. Use concrete questions, one per line when there are several, and number them so they are easy to answer. + +Example of a well-formed block: + +> Before writing the data model I need to clarify three things: +> +> 1. **Persistence.** localStorage, IndexedDB, or a JSON file on disk? Recommendation: localStorage if the data fits in <5MB and does not need queries. +> 2. **Schema versioning.** What happens when the format changes? Options: (a) version prefix in the key, (b) ignore and rebuild, (c) migrate on load. +> 3. **Privacy.** Is the data sensitive? If yes, is it encrypted? Is it deleted on logout? + +## Arguments + +`$ARGUMENTS` is **the feature description**, not the file name. Treat it as the starting point for Phase 1 and derive the slug from the objective in Phase 4. + +The one exception: if `$ARGUMENTS` is already a single kebab-case token with no spaces (e.g. `/spec levels-and-highscores`), it is ambiguous between a description and a slug — use it as the slug **and** as the seed of the description, without asking for confirmation. + +If they invoked `/spec` without arguments, start by asking for the one-sentence description. diff --git a/.agents/skills/spec/template.md b/.agents/skills/spec/template.md new file mode 100644 index 0000000..26ccaf4 --- /dev/null +++ b/.agents/skills/spec/template.md @@ -0,0 +1,182 @@ +# Template for a useful spec + +This file is the reference the `/spec` skill consults when generating specs. Each section includes its purpose and a minimal example. **It is not text to be copied verbatim** — it is the shape the skill must respect. + +--- + +## Header + +Every spec starts with metadata in a blockquote (no tables, no blocks, simple as shown below) format: + +```markdown +# SPEC NN — Short, descriptive title + +> **Status:** Draft +> **Depends on:** SPEC 01, SPEC 02 +> **Date:** YYYY-MM-DD +> **Objective:** A single sentence. If you need two sentences, the feature is too big. +``` + +**Valid states:** `Draft`, `In review`, `Approved`, `Implemented`, `Obsolete`. + +> The labels above are the English defaults. The skills also accept equivalents in any language (e.g. Spanish `Borrador` / `En revisión` / `Aprobado` / `Implementado` / `Obsoleto`). Pick one set per repo and stay consistent. + +**Objective rule:** one sentence that a human reads in 5 seconds and understands what is going to be built. If it doesn't fit in one sentence, split the feature. + +--- + +## Section 1 — Why this spec exists (optional) + +For specs that take non-obvious decisions or break project patterns, a brief section explaining the **why** of the work. Not the what — the what comes later. + +For simple specs, omit it. + +--- + +## Section 2 — Scope + +Two explicit sub-blocks. **Both are mandatory.** + +```markdown +## Scope + +**In:** + +- Concrete thing one. +- Concrete thing two. + +**Out of scope (for future specs):** + +- Something that could be done but not now. +- Something that came up in the conversation but is not in. +``` + +**Why "out" matters:** it captures the things the user mentioned during the question phase but were decided to be deferred. Without that record, during implementation there will be a temptation to slip them in "while we're at it". + +--- + +## Section 3 — Data model + +The concrete structures that appear or change. Use real code, not abstract pseudocode. + +```markdown +## Data model + +\`\`\`js +// Game state +const state = { +level: 1, +score: 0, +highScores: [/* { score, level, date } */], +}; +\`\`\` + +Conventions: + +- Coordinates: origin top-left. +- Velocities in pixels/frame. +``` + +If the feature introduces no new data, write it explicitly: _"This feature introduces no new data structures. It reuses the model from SPEC 01."_ + +--- + +## Section 4 — Implementation plan + +Numbered steps. Each step must leave the system in a **functional and runnable** state. No "implement half and continue tomorrow". + +```markdown +## Implementation plan + +1. Create file X with an empty skeleton. +2. Implement function A in X. Manual test: run Y, see Z. +3. Wire X to existing module W. +4. ... +``` + +**Rules:** + +- Each step must be commitable on its own. +- If a step requires more than 30–50 lines of code, split it. +- The last step of the plan is **not** "test everything" — that is the acceptance criteria. + +--- + +## Section 5 — Acceptance criteria + +Boolean checklist. Each item can be verified with yes or no. + +```markdown +## Acceptance criteria + +- [ ] The game loads without errors in the console. +- [ ] Breaking a brick adds exactly 10 points. +- [ ] Reloading the page preserves the high-scores. +``` + +**Anti-patterns to avoid:** + +- ❌ "That it works well." → not verifiable. +- ❌ "Good UX." → subjective. +- ❌ "No bugs." → not operational. +- ✅ "Pressing Esc pauses the game and shows the menu." → verifiable, boolean. + +--- + +## Section 6 — Decisions taken and discarded + +The section that has the most value 3 months from now. Capture **what you considered**, not just what you chose. + +```markdown +## Decisions + +- **Yes:** localStorage for persistence. Fits in <5MB and we don't need queries. +- **No:** IndexedDB. Overengineering for this case. +- **Yes:** versioned key (`save:v1`). Lets us migrate the schema later without breaking. +- **No:** cloud sync. Goes in another spec if it ever lands. +``` + +Each decision ideally has a brief reason. Decisions without a reason are the first ones to be questioned later. + +--- + +## Section 7 — Identified risks (optional) + +Only when there are non-obvious risks. Simple table: + +```markdown +## Risks + +| Risk | Mitigation | +| ------------------------------------- | --------------------------------------------------------------------------- | +| localStorage disabled in private mode | Fallback to in-memory object. The game still runs, it just doesn't persist. | +| Future incompatible schema | Key includes `:v1`. Migration documented in `persistence.js`. | +``` + +For small specs or very contained features, omit it. + +--- + +## Final section — What is NOT in (reinforcement) + +Repeat explicitly at the end what **will not** be done in this spec. This repetition is deliberate — the Scope section already says it, but at the end of the document it serves as a reminder when someone reads only the last lines. + +```markdown +## What is **not** in this spec + +- Visual editor (another spec if it ever lands). +- Multiplayer. +- Mobile version. + +Each one of those, if it lands, goes in its own spec. +``` + +--- + +## Global rules about the whole document + +- **One sentence per idea.** If a sentence has two commas and a semicolon, split it. +- **Concrete names.** If you say "the levels module", say `src/levels.js`. If you say "a key", give the exact string. +- **No TODOs.** A TODO in a spec means the decision was not made. Make it or note it as a pending decision with a reason. +- **No long executable code.** The spec describes; the code is written afterwards. Short snippets to illustrate data structures are fine; full functions are not. +- **Standard markdown.** No weird extensions. It must render on GitHub without surprises. diff --git a/references/pantallas/activar-cuenta.dc.html b/references/pantallas/activar-cuenta.dc.html new file mode 100644 index 0000000..0420d6d --- /dev/null +++ b/references/pantallas/activar-cuenta.dc.html @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + +
+
+
+ +
+

Bienvenida a OpenDayCare

+

Te invitaron a seguir el día de tu hijo. Creá tu contraseña para activar la cuenta.

+ +
+
M
+
Te invitaron a seguir a
Mateo · Sala Soles
+
+ +
CÓDIGO DE INVITACIÓN
+ +
EMAIL
+ +
CREAR CONTRASEÑA
+ + + + + Activar mi cuenta +

¿Ya tenés cuenta? Iniciar sesión

+
+
+ + +
+ + + diff --git a/references/pantallas/agregar-nino.dc.html b/references/pantallas/agregar-nino.dc.html new file mode 100644 index 0000000..c0ed880 --- /dev/null +++ b/references/pantallas/agregar-nino.dc.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + +
+
+
+ Cancelar + Agregar niño + Guardar +
+
+
NOMBRE COMPLETO
+ +
+
FECHA DE NACIMIENTO
+
SALA
Soles
+
+
ALERGIAS (ETIQUETAS)
+ +
NOTAS MÉDICAS
+ +
+
+
+ + +
+ + + diff --git a/references/pantallas/avisos.dc.html b/references/pantallas/avisos.dc.html new file mode 100644 index 0000000..cad07fb --- /dev/null +++ b/references/pantallas/avisos.dc.html @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+
ACTIVIDAD

Avisos

+
+
L
Lucía Fernández comentó en la publicación de Mateo.
Hace 12 min
+
Diego Fernández activó su cuenta y ya sigue a Mateo.
Hace 1 h
+
Carla Méndez reaccionó a la publicación de Sofía.
Hace 2 h
+
Recordá enviar el resumen del día de la sala Soles.
Hoy 17:00
+
+
+
+
+ + +
+ + + diff --git a/references/pantallas/crear-publicacion.dc.html b/references/pantallas/crear-publicacion.dc.html new file mode 100644 index 0000000..47dd054 --- /dev/null +++ b/references/pantallas/crear-publicacion.dc.html @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + +
+
+
+ Cancelar + Nueva publicación + Publicar +
+
+
PARA
+
+ + + + +
+
TIPO
+
+ + + + + + + +
+
DESCRIPCIÓN
+ +
FOTOS
+
+
+
Agregar
+
+
+
+
+ + +
+ + + diff --git a/references/pantallas/detalle-publicacion.dc.html b/references/pantallas/detalle-publicacion.dc.html new file mode 100644 index 0000000..eefda82 --- /dev/null +++ b/references/pantallas/detalle-publicacion.dc.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+ Volver al feed +
+
M
Mateo
09:40 · Maestra Caro · Sala Soles
ACTIVIDAD
+

Pintamos con témperas esta mañana. Mateo eligió el azul para todo y se concentró un montón mezclando colores.

+ Foto · pintando con témperas +
A 5 personas les gusta
+
+ +
Comentar
+
+
+ +
COMENTARIOS · 2
+
+
L
Lucía Fernández· mamá10:02
¡Qué hermoso! En casa también le encanta el azul.
+
C
Maestra Caro· maestra10:15
¡Se nota mucho! Le guardamos la obra 🎨
+
+
+
L
+ + +
+
+
+
+ + +
+ + + diff --git a/references/pantallas/familia-cuenta.dc.html b/references/pantallas/familia-cuenta.dc.html new file mode 100644 index 0000000..5e5cb97 --- /dev/null +++ b/references/pantallas/familia-cuenta.dc.html @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+
AJUSTES

Mi cuenta

+
+
L
+
Lucía Fernández
lucia.fernandez@gmail.com
Mamá de Mateo y Sofía
+ +
+
+
MIS HIJOS
+
+
M
Mateo Fernández
3 años · Sala Soles
Fotos
+
S
Sofía Méndez
2 años · Sala Soles
Fotos
+
+
+
+
NOTIFICACIONES
+
Avisos cuando se publica algo
+
Resumen diario (19:00)
+
+
+
Cambiar contraseña
+
Ayuda y soporte
+
+ Cerrar sesión +
+
+
+ + +
+ + + diff --git a/references/pantallas/familia-feed.dc.html b/references/pantallas/familia-feed.dc.html new file mode 100644 index 0000000..44b1eb5 --- /dev/null +++ b/references/pantallas/familia-feed.dc.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+
+
TU FAMILIA
+

Hola, Lucía

+

Así va el día de hoy

+
+
+ + + +
+
HOY · MARTES 17 JUN
+ + +
+
+
+ + +
+ + + diff --git a/references/pantallas/feed.dc.html b/references/pantallas/feed.dc.html new file mode 100644 index 0000000..e7afef0 --- /dev/null +++ b/references/pantallas/feed.dc.html @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+
+
GUARDERÍA · SALA SOLES
+

Buenas, Caro

+

12 niños · martes 17 jun

+
+ + +
C
+ Compartí un momento… + +
+ +
PUBLICADO HOY
+ +
+ +
+
+
M
+
Mateo
14:20 · publicado por vos
+
LOGRO
+
+
Para: familia de Mateo
+

¡Usó el orinal solito por primera vez! Estaba feliz de contárselo a todos. Un gran paso.

+
+ 3 + 1 + + Editar +
+
+ +
+
+
M
+
Mateo
09:40 · publicado por vos
+
ACTIVIDAD
+
+
Para: familia de Mateo
+

Pintamos con témperas esta mañana. Mateo eligió el azul para todo y se concentró un montón mezclando colores.

+ Foto · pintando con témperas +
+ 5 + 2 + + Editar +
+
+ +
+
+
+
Anuncio general
07:50 · publicado por vos
+
ANUNCIO
+
+
Para: toda la sala
+

El viernes salimos al parque por la mañana. Recuerden mandar gorra y una botellita de agua.

+
+ 8 + 0 + + Editar +
+
+
+
+
+
+ + +
+ + + diff --git a/references/pantallas/foto.dc.html b/references/pantallas/foto.dc.html new file mode 100644 index 0000000..ef39f3e --- /dev/null +++ b/references/pantallas/foto.dc.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + +
+
+ +
Mateo
09:40 · Maestra Caro · Sala Soles
+ +
+
+
+ + Foto · pintando con témperas +
+
+

Pintamos con témperas esta mañana. Mateo eligió el azul para todo y se concentró un montón mezclando colores.

+
+ + +
+ + + diff --git a/references/pantallas/index.dc.html b/references/pantallas/index.dc.html new file mode 100644 index 0000000..07f36af --- /dev/null +++ b/references/pantallas/index.dc.html @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + +
+
+
+ + + + +
+
+

+ OpenDayCare · Pantallas +

+

+ Haz click en la pantalla que quieras ver +

+
+
+ +
+ ACCESO +
+
+ 01 +
+
+ Iniciar sesión +
+
+ Acceso staff o familia +
+
+ 02 +
+
+ Activar cuenta +
+
+ Padre · código de invitación +
+
+
+ +
+ GUARDERÍA · STAFF +
+
+ 03 +
+
+ Feed de la guardería +
+
+ Muro de publicaciones +
+
+ 04 +
+
+ Crear publicación +
+
Nuevo momento
+
+ 05 +
+
Niños
+
+ Gestión de la sala +
+
+ 06 +
+
+ Perfil del niño +
+
+ Datos, alergias, padres +
+
+ 07 +
+
+ Agregar / editar niño +
+
Formulario
+
+ 08 +
+
+ Vincular padre +
+
+ Invitación por código +
+
+ 09 +
+
Avisos
+
+ Actividad reciente +
+
+ 10 +
+
+ Mi cuenta (staff) +
+
Ajustes
+
+
+ +
+ FAMILIA · PADRE +
+
+ 11 +
+
+ Feed del padre +
+
+ El día de sus hijos +
+
+ 12 +
+
+ Detalle de publicación +
+
+ Reacciones y comentarios +
+
+ 13 +
+
+ Foto a pantalla completa +
+
Visor de imagen
+
+ 14 +
+
+ Resumen del día +
+
Cierre del día
+
+ 15 +
+
+ Mi cuenta (familia) +
+
+ Ajustes y consentimiento +
+
+
+
+ + +
+ + + diff --git a/references/pantallas/login.dc.html b/references/pantallas/login.dc.html new file mode 100644 index 0000000..fefb48d --- /dev/null +++ b/references/pantallas/login.dc.html @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ +
+ OpenDayCare +
+
+

El día de cada niño,
compartido con su familia.

+

Publicá momentos, gestioná las salas y mantené a las familias cerca, desde un solo lugar.

+
+
🌿 Guardería Sala Soles
+
+ +
+
+

Iniciar sesión

+

Ingresá para ver el día de hoy.

+ +
INGRESO COMO
+
+ + +
+ +
EMAIL
+ +
CONTRASEÑA
+ +
¿Olvidaste tu contraseña?
+ + Iniciar sesión + +

¿Te invitó la guardería? Activá tu cuenta

+
+
+
+ + +
+ + + diff --git a/references/pantallas/mi-cuenta.dc.html b/references/pantallas/mi-cuenta.dc.html new file mode 100644 index 0000000..0467610 --- /dev/null +++ b/references/pantallas/mi-cuenta.dc.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+
AJUSTES

Mi cuenta

+
+
C
+
Caro Giménez
caro@opendaycare.com
Maestra · Sala Soles
+ +
+
+
NOTIFICACIONES
+
Nuevos comentarios de familias
+
Recordatorio de resumen (17:00)
+
+
+
Cambiar contraseña
+
Ayuda y soporte
+
+ Cerrar sesión +
+
+
+ + +
+ + + diff --git a/references/pantallas/ninos.dc.html b/references/pantallas/ninos.dc.html new file mode 100644 index 0000000..afca52a --- /dev/null +++ b/references/pantallas/ninos.dc.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+
+
GESTIÓN

Niños

+ Agregar niño +
+
+ + +
+
SALA SOLES8 niños
+ +
+
+
+ + +
+ + + diff --git a/references/pantallas/perfil-nino.dc.html b/references/pantallas/perfil-nino.dc.html new file mode 100644 index 0000000..647189e --- /dev/null +++ b/references/pantallas/perfil-nino.dc.html @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+ Volver a Niños +
+
+
+
M
+

Mateo Fernández

3 años · Sala Soles

+ Editar +
+
+
+
Alergias y notas
Alergia al maní. Evitar frutos secos. Lleva inhalador en la mochila.
+
+
+
Fecha de nacimiento12 mar 2022
+
SalaSoles
+
Ingresofeb 2025
+
+
+
+ Resumen del día +
+
PADRES VINCULADOS
+
+
L
Lucía Fernández
Mamá · activa
ACTIVA
+
D
Diego Fernández
Papá · invitación enviada
PENDIENTE
+ Vincular otro padre +
+
+
+
+
+
+
+ + +
+ + + diff --git a/references/pantallas/resumen-dia.dc.html b/references/pantallas/resumen-dia.dc.html new file mode 100644 index 0000000..c082320 --- /dev/null +++ b/references/pantallas/resumen-dia.dc.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+
+ + +
+
+
RESUMEN DEL DÍA
+

El día de Mateo

+

martes 17 de junio

+
+
+
3
comidas
+
1h 30
de siesta
+
4
momentos
+
+
Ánimo del día
Contento y participativo
+
LO MÁS LINDO DE HOY
+ +
+
+
+ + +
+ + + diff --git a/references/pantallas/support.js b/references/pantallas/support.js new file mode 100644 index 0000000..d2f00df --- /dev/null +++ b/references/pantallas/support.js @@ -0,0 +1,1658 @@ +// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/react.ts + function getReact() { + const R = window.React; + if (!R) throw new Error("dc-runtime: window.React is not available yet"); + return R; + } + function getReactDOM() { + const RD = window.ReactDOM; + if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); + return RD; + } + var h = ((...args) => getReact().createElement( + ...args + )); + + // src/parse.ts + function parseDcDocument(doc) { + const dc = doc.querySelector("x-dc"); + if (!dc) return null; + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template: dc.innerHTML, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDcText(src) { + const openMatch = /]*)?>/.exec(src); + if (!openMatch) return null; + const close = src.lastIndexOf(""); + if (close === -1 || close < openMatch.index) return null; + const template = src.slice(openMatch.index + openMatch[0].length, close); + const doc = new DOMParser().parseFromString(src, "text/html"); + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDataProps(raw) { + if (!raw) return { props: null, preview: null }; + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { props: null, preview: null }; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { props: null, preview: null }; + } + const obj = parsed; + const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; + const rest = {}; + for (const k of Object.keys(obj)) { + if (k[0] !== "$") rest[k] = obj[k]; + } + return { props: Object.keys(rest).length ? rest : null, preview }; + } + function dcNameFromPath(pathname) { + let p = pathname || ""; + try { + p = decodeURIComponent(p); + } catch { + } + const base = p.split("/").pop() || "Root"; + return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; + } + + // src/boot.ts + var BASE_CSS = ` + .sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent); + border:1px solid color-mix(in srgb,currentColor 50%,transparent); + border-radius:2px;box-sizing:border-box;overflow:hidden} + @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} + html.sc-dc-streaming .sc-placeholder, + html.sc-dc-streaming .sc-interp.sc-missing{position:relative; + background:color-mix(in srgb,currentColor 5%,transparent); + border-color:transparent} + html.sc-dc-streaming .sc-placeholder::before, + html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; + position:absolute;inset:0;pointer-events:none; + background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); + background-size:400% 100%;animation:sc-shine 1.4s ease infinite} + html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, + html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; + background:color-mix(in srgb,currentColor 8%,transparent)} + .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; + color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word} + .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; + vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;color:transparent; + user-select:none} + .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; + color:color-mix(in srgb,currentColor 50%,transparent); + background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px; + padding:0 3px} + .sc-host.sc-has-error{position:relative} + .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; + padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; + border-radius:4px;white-space:pre-wrap;pointer-events:none} + /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both + in sync until dc-runtime regains a build step. */ + @media print { + @page { margin: 0.5cm; } + figure, table { break-inside: avoid; } + #dc-root, #dc-root > .sc-host { height: auto; } + *, *::before, *::after { + print-color-adjust: exact; -webkit-print-color-adjust: exact; + backdrop-filter: none !important; -webkit-backdrop-filter: none !important; + animation-delay: -99s !important; animation-duration: .001s !important; + animation-iteration-count: 1 !important; animation-fill-mode: both !important; + animation-play-state: running !important; transition-duration: 0s !important; + } + } + `; + var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; + function rootNameForDocument(doc, loc) { + let bootPath = loc.pathname || ""; + if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { + try { + bootPath = new URL(doc.baseURI || "/").pathname; + } catch { + } + } + return dcNameFromPath(bootPath); + } + function safeDecode(s) { + try { + return decodeURIComponent(s); + } catch { + return s; + } + } + function boot(runtime, doc = document) { + const parsed = parseDcDocument(doc); + if (!parsed) return null; + const React = getReact(); + const rootName = rootNameForDocument(doc, location); + runtime.markFetched(rootName); + runtime.setRootName(rootName); + runtime.adoptParsed(rootName, parsed); + fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { + const raw = t ? parseDcText(t) : null; + if (raw?.template) runtime.updateHtml(rootName, raw.template); + }).catch(() => { + }); + const dc = doc.querySelector("x-dc"); + const hostEl = doc.createElement("div"); + hostEl.id = "dc-root"; + dc.replaceWith(hostEl); + if (!parsed.preview) { + const s = doc.createElement("style"); + s.textContent = FULL_PAGE_CSS; + doc.head.appendChild(s); + } + const Root = runtime.getDC(rootName); + const entry = runtime.registry.get(rootName); + function StandaloneRoot() { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + entry.subs.add(sub); + return () => { + entry.subs.delete(sub); + }; + }, []); + const defaults = React.useMemo(() => { + const d = {}; + for (const k in entry.propsMeta || {}) { + const v = entry.propsMeta?.[k]?.default; + if (v !== void 0) d[k] = v; + } + return d; + }, [entry.propsMeta]); + return h(Root, { ...defaults, ...entry.propOverrides || {} }); + } + const ReactDOM = getReactDOM(); + if (ReactDOM.createRoot) + ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); + else ReactDOM.render(h(StandaloneRoot), hostEl); + return rootName; + } + + // src/expr.ts + var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; + var NUMBER_RE = /^-?\d+(\.\d+)?$/; + function resolve(vals, src) { + const expr = String(src).trim(); + if (!expr) return void 0; + if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { + return resolve(vals, expr.slice(1, -1)); + } + const eq = findTopLevelEquality(expr); + if (eq) { + const lv = resolve(vals, expr.slice(0, eq.index)); + const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); + switch (eq.op) { + case "===": + return lv === rv; + case "!==": + return lv !== rv; + case "==": + return lv == rv; + default: + return lv != rv; + } + } + if (expr[0] === "!") return !resolve(vals, expr.slice(1)); + if (expr === "true") return true; + if (expr === "false") return false; + if (expr === "null") return null; + if (expr === "undefined") return void 0; + if (NUMBER_RE.test(expr)) return Number(expr); + if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { + return expr.slice(1, -1); + } + return resolvePath(vals, expr); + } + function parensWrapWhole(expr) { + let depth = 0; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") depth++; + else if (expr[i] === ")") { + depth--; + if (depth === 0) return false; + } + } + return true; + } + function findTopLevelEquality(expr) { + let depth = 0; + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + if (c === "[" || c === "(") depth++; + else if (c === "]" || c === ")") depth--; + else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { + if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; + if (!expr.slice(0, i).trim()) continue; + const op = expr[i + 2] === "=" ? c + "==" : c + "="; + return { index: i, op }; + } + } + return null; + } + function resolvePath(vals, expr) { + const head = expr.match(IDENT_RE); + if (!head) return void 0; + let cur = vals == null ? void 0 : vals[head[0]]; + let i = head[0].length; + while (i < expr.length) { + if (expr[i] === ".") { + const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); + if (!m) return void 0; + cur = cur == null ? void 0 : cur[m[0]]; + i += 1 + m[0].length; + } else if (expr[i] === "[") { + let depth = 1; + let j = i + 1; + while (j < expr.length && depth > 0) { + if (expr[j] === "[") depth++; + else if (expr[j] === "]") { + depth--; + if (depth === 0) break; + } + j++; + } + if (depth !== 0) return void 0; + const key = resolve(vals, expr.slice(i + 1, j)); + cur = cur == null ? void 0 : cur[key]; + i = j + 1; + } else { + return void 0; + } + } + return cur; + } + + // src/encode.ts + var CAMEL_ATTR = "sc-camel-"; + var INLINE_TEXT_TAGS = new Set( + "a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr".split( + " " + ) + ); + var RAW_WRAP = { + select: "sc-raw-select", + table: "sc-raw-table", + tbody: "sc-raw-tbody", + thead: "sc-raw-thead", + tfoot: "sc-raw-tfoot", + tr: "sc-raw-tr", + td: "sc-raw-td", + th: "sc-raw-th", + caption: "sc-raw-caption" + }; + var RAW_UNWRAP = Object.fromEntries( + Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) + ); + var EVENT_MAP = { + onclick: "onClick", + onchange: "onChange", + oninput: "onInput", + onsubmit: "onSubmit", + onkeydown: "onKeyDown", + onkeyup: "onKeyUp", + onkeypress: "onKeyPress", + onmousedown: "onMouseDown", + onmouseup: "onMouseUp", + onmouseenter: "onMouseEnter", + onmouseleave: "onMouseLeave", + onfocus: "onFocus", + onblur: "onBlur", + ondoubleclick: "onDoubleClick", + oncontextmenu: "onContextMenu" + }; + var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; + var IMPORT_SELF_CLOSE_RE = new RegExp( + "<(x-import|dc-import)(" + ATTRS + ")/>", + "gi" + ); + var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; + function encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + for (const [real, alias] of Object.entries(RAW_WRAP)) { + html = html.replace( + new RegExp("(])", "gi"), + "$1" + alias + ); + } + return html; + } + function kebabToCamel(s) { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + } + function cssToObj(css) { + const o = {}; + for (const decl of css.split(";")) { + const i = decl.indexOf(":"); + if (i < 0) continue; + const prop = decl.slice(0, i).trim(); + o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); + } + return o; + } + function compileAttr(raw) { + const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); + if (whole) { + const path = whole[1]; + return (vals) => resolve(vals, path); + } + if (raw.includes("{{")) { + const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); + return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); + } + return () => raw; + } + + // src/compile.ts + function collectProps(node, kind, host) { + const propGetters = []; + const pseudoClasses = []; + let hintSize = null; + for (const { name, value } of [...node.attributes]) { + if (name === "sc-name" || name === "data-dc-tpl") continue; + let key = name; + if (key.startsWith(CAMEL_ATTR)) + key = kebabToCamel(key.slice(CAMEL_ATTR.length)); + if (key === "hint-size") { + hintSize = value; + continue; + } + if (key.startsWith("style-")) { + pseudoClasses.push(host.pseudoClass(key.slice(6), value)); + continue; + } + if (kind !== "dom") { + if (key.includes("-") && !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-")))) + key = kebabToCamel(key); + } else { + if (key === "class") key = "className"; + else if (key === "for") key = "htmlFor"; + else if (key.startsWith("on")) + key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); + } + propGetters.push([key, compileAttr(value)]); + } + return { propGetters, pseudoClasses, hintSize }; + } + var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ + "position", + "left", + "right", + "top", + "bottom", + "inset", + "width", + "height", + "z-index", + "transform" + ]); + function hostPositionStyle(style) { + const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; + if (!all) return void 0; + const out = {}; + for (const [k, v] of Object.entries(all)) { + const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); + if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; + } + return Object.keys(out).length ? out : void 0; + } + function compileTemplate(html, host) { + const tpl = document.createElement("template"); + //! nosemgrep: direct-inner-html-assignment + tpl.innerHTML = encodeCase(html); + let tplN = 0; + (function stamp(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + node.setAttribute("data-dc-tpl", String(tplN++)); + } + for (const c of node.childNodes) stamp(c); + })(tpl.content); + const builders = walkChildren(tpl.content, host); + const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); + render.__annotated = tpl.innerHTML; + return render; + } + function walkChildren(node, host) { + return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); + } + function walk(node, host) { + if (node.nodeType === Node.TEXT_NODE) return walkText(node); + if (node.nodeType !== Node.ELEMENT_NODE) return null; + const el = node; + const tag = el.tagName.toLowerCase(); + if (tag === "sc-for") return walkFor(el, host); + if (tag === "sc-if") return walkIf(el, host); + if (tag === "x-import") return walkXImport(el, host); + if (tag === "sc-helmet") return host.helmet(el); + if (tag === "dc-import") return walkComponent(el, host); + return walkElement(el, host); + } + var warnedHoles = /* @__PURE__ */ new Set(); + function warnUnresolved(ctx, what) { + const key = (ctx?.__name || "?") + "\0" + what; + if (warnedHoles.has(key)) return; + warnedHoles.add(key); + console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); + } + function walkText(node) { + const txt = node.nodeValue ?? ""; + if (!txt.includes("{{")) { + if (!txt.trim() && !txt.includes(" ")) return null; + return () => txt; + } + const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); + return (vals, ctx, key) => h( + getReact().Fragment, + { key }, + ...parts.map((p, i) => { + if (!(i & 1)) return p; + const v = resolve(vals, p); + if (v === void 0) { + if (!ctx?.__streamingNow) { + if (document.body?.hasAttribute("data-dc-editor-on")) { + return h( + "span", + { key: i, className: "sc-interp sc-unresolved" }, + "{{ " + p.trim() + " }}" + ); + } + warnUnresolved( + ctx, + "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" + ); + return null; + } + return h( + "span", + { key: i, className: "sc-interp sc-missing" }, + p.trim() + ); + } + if (getReact().isValidElement(v) || Array.isArray(v)) { + return h(getReact().Fragment, { key: i }, v); + } + if (v === null || typeof v === "boolean") return null; + return h("span", { key: i, className: "sc-interp" }, String(v)); + }) + ); + } + function walkFor(el, host) { + const listGet = compileAttr(el.getAttribute("list") || ""); + const asName = el.getAttribute("as") || "item"; + const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); + const kids = walkChildren(el, host); + const listSrc = el.getAttribute("list") || ""; + return (vals, ctx, key) => { + let list = listGet(vals); + if (!Array.isArray(list)) { + if (!ctx?.__streamingNow) { + if (list !== void 0 && list !== null) { + warnUnresolved( + ctx, + 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" + ); + } + list = []; + } else { + list = hintN > 0 ? Array(hintN).fill(void 0) : []; + } + } + return h( + getReact().Fragment, + { key }, + list.map((item, i) => { + const sub = { ...vals, [asName]: item, $index: i }; + return h( + getReact().Fragment, + { key: i }, + kids.map((b, j) => b(sub, ctx, j)) + ); + }) + ); + }; + } + function walkIf(el, host) { + const valGet = compileAttr(el.getAttribute("value") || ""); + const hintRaw = el.getAttribute("hint-placeholder-val"); + const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + let v = valGet(vals); + if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); + return v ? h( + getReact().Fragment, + { key }, + kids.map((b, j) => b(vals, ctx, j)) + ) : null; + }; + } + function walkComponent(el, host) { + const name = el.getAttribute("name") || el.getAttribute("component") || ""; + el.removeAttribute("name"); + el.removeAttribute("component"); + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const { propGetters, hintSize } = collectProps(el, "dc-import", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key, + __hintSize: hintSize, + __tplId: tplId, + __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 + }; + for (const [k, g] of propGetters) { + const v = g(vals); + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return h(host.component(name), props); + }; + } + function walkXImport(el, host) { + const globalNameGet = compileAttr( + el.getAttribute("component-from-global-scope") || "" + ); + const exportNameGet = compileAttr( + el.getAttribute("component") || el.getAttribute("name") || "" + ); + const fromRaw = el.getAttribute("from") || el.getAttribute("src") || el.getAttribute("import") || ""; + const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; + const url = urls.length ? urls[urls.length - 1] : ""; + const kindOf = (u) => /\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js"; + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const wrap = tplId != null || styleGet != null; + const { propGetters, hintSize } = collectProps(el, "x-import", host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const kids = hasContent ? walkChildren(el, host) : []; + const urlBindable = fromRaw.includes("{{"); + if (urls.length && !urlBindable) { + let prev; + for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); + } + const evalName = (g, vals) => { + const v = g(vals); + const s = v == null ? "" : String(v); + return s.includes("{{") ? "" : s; + }; + return (vals, ctx, key) => { + const globalName = evalName(globalNameGet, vals); + const name = globalName || evalName(exportNameGet, vals); + const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); + const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; + const wrapper = wrap ? { + key, + className: "sc-host-x", + "data-dc-tpl": tplId, + style: hostStyle || { display: "contents" } + } : null; + if (!C) { + const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + const props = wrapper ? {} : { key }; + let unresolvedHole = false; + for (const [k, g] of propGetters) { + if (k === "component" || k === "componentFromGlobalScope" || k === "from") { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (unresolvedHole && ctx?.__htmlStreamingNow) { + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error: null + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function contentKey(el) { + const clone = el.cloneNode(true); + for (const d of clone.querySelectorAll("*")) { + while (d.attributes.length) d.removeAttribute(d.attributes[0].name); + } + const s = clone.innerHTML; + let h2 = 5381; + for (let i = 0; i < s.length; i++) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0; + return s.length + "." + (h2 >>> 0).toString(36); + } + var NEVER_CONTENT_KEYED = new Set( + "script style textarea option title select canvas iframe video audio".split( + " " + ) + ); + var NOT_INLINE_SELECTOR = ":not(" + [...INLINE_TEXT_TAGS].join(",") + ")"; + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const inlineOnly = el.childNodes.length > 0 && !NEVER_CONTENT_KEYED.has(realTag) && el.querySelector(NOT_INLINE_SELECTOR) === null; + const keySuffix = inlineOnly ? "|" + contentKey(el) : ""; + const { propGetters, pseudoClasses } = collectProps(el, "dom", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key: key + keySuffix, + "data-dc-tpl": tplId + }; + for (const [k, g] of propGetters) { + let v = g(vals); + if (k === "style" && typeof v === "string") v = cssToObj(v); + if ((k === "value" || k === "checked") && v === void 0) { + v = k === "checked" ? false : ""; + } + props[k] = v; + } + if (pseudoClasses.length) { + props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); + } + return h(realTag, props, ...kids.map((b, j) => b(vals, ctx, j))); + }; + } + + // src/logic.ts + var StreamableLogic = class { + constructor(props) { + __publicField(this, "props"); + __publicField(this, "state", {}); + /** Back-pointer to the wrapper component, installed after construction. */ + __publicField(this, "__host"); + this.props = props || {}; + } + setState(update, cb) { + this.__host && this.__host.__setLogicState(update, cb); + } + forceUpdate() { + this.__host && this.__host.forceUpdate(); + } + componentDidMount() { + } + componentDidUpdate(_prevProps) { + } + componentWillUnmount() { + } + /** The flat object the template renders against (merged over props). */ + renderVals() { + return {}; + } + }; + function evalDcLogic(src) { + //! nosemgrep: eval-and-function-constructor + const fn = new Function( + "DCLogic", + "StreamableLogic", + "React", + src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' + ); + return fn(StreamableLogic, StreamableLogic, getReact()); + } + + // src/component.ts + function shallowEqual(a, b) { + if (!b) return false; + const ak = Object.keys(a).filter((k) => k !== "children"); + const bk = Object.keys(b).filter((k) => k !== "children"); + if (ak.length !== bk.length) return false; + for (const k of ak) if (a[k] !== b[k]) return false; + return true; + } + function Placeholder({ + name, + hintSize, + streaming, + error + }) { + const [w, hgt] = (hintSize || "100%,60px").split(","); + return h( + "div", + { + className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), + style: { width: w.trim(), height: hgt && hgt.trim() }, + title: name + }, + error ? h( + "div", + { className: "sc-placeholder-error" }, + (name ? name + ": " : "") + error + ) : null + ); + } + function hintToMin(hint) { + if (!hint) return void 0; + const [w, hgt] = hint.split(","); + return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; + } + function createComponentFactory(registry, ensureFetched) { + const React = getReact(); + const AncestorContext = React.createContext([]); + class StreamableComponent extends React.Component { + constructor(props) { + super(props); + __publicField(this, "__name"); + __publicField(this, "__sub"); + __publicField(this, "__needsDidMount", false); + /** Snapshot of the registry's streaming flags taken at render time — + * builders read it off the RenderCtx (this) to pick placeholder vs + * render-nothing for unresolved values. */ + __publicField(this, "__streamingNow", false); + __publicField(this, "__htmlStreamingNow", false); + /** When a construct throws, remember the (class, registry.ver, props) + * triple so render-time reconcile doesn't re-attempt it on every parent + * re-render. A registry bump (new class, template, external module + * resolving via bumpAll) changes `ver` and breaks the memo so an + * env-dependent constructor can self-heal. */ + __publicField(this, "__failedLogic", null); + __publicField(this, "__failedUserProps", null); + __publicField(this, "__failedVer", -1); + /** Per-instance constructor error — kept here (not on the registry entry) + * so one instance's successful construct can't hide a sibling's failure, + * and a construct can never wipe an eval error `updateJs` recorded on + * `r.logicError`. */ + __publicField(this, "__ctorError", null); + __publicField(this, "logic"); + this.__name = props.__name; + this.state = { __v: 0, __err: null }; + this.__sub = () => { + if (this.state.__err) this.setState({ __err: null }); + this.forceUpdate(); + }; + this.__makeLogic(registry.get(this.__name).Logic, null); + ensureFetched(this.__name); + } + /** Error-boundary hook: a render crash anywhere in this DC's subtree + * (its own template, an x-import'd component, a child DC without its + * own deeper boundary) lands here instead of unmounting the page. */ + static getDerivedStateFromError(e) { + return { __err: e instanceof Error && e.message ? e.message : String(e) }; + } + componentDidCatch(e, info) { + console.error( + "[dc-runtime] render error in <" + this.__name + ">:", + e, + info?.componentStack || "" + ); + } + /** Instantiate the logic class (or the no-op base) and adopt `prevState` + * over its initial state — used both at mount and on hot-swap. */ + __makeLogic(Logic, prevState) { + const L = Logic || StreamableLogic; + try { + this.logic = new L(this.__userProps()); + this.__failedLogic = null; + this.__failedUserProps = null; + this.__ctorError = null; + } catch (e) { + console.error(e); + this.__failedLogic = Logic; + this.__failedUserProps = this.__userProps(); + this.__failedVer = registry.get(this.__name).ver; + this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); + this.logic = new StreamableLogic( + this.__userProps() + ); + } + this.logic.__host = this; + if (prevState) + this.logic.state = { ...this.logic.state || {}, ...prevState }; + } + /** The props the author's logic + template see — internal __-prefixed + * wiring stripped. */ + __userProps() { + const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; + return rest; + } + __setLogicState(update, cb) { + const prev = this.logic.state; + const patch = typeof update === "function" ? update(prev) : update; + this.logic.state = { ...prev, ...patch }; + this.setState((s) => ({ __v: s.__v + 1 }), cb); + } + /** Swap the logic instance when the registry's Logic class changed + * (streaming completion, hot reload). State carries over; didMount + * re-fires after the swap commits so refs exist. */ + __reconcileLogic() { + const r = registry.get(this.__name); + const Next = r.Logic; + const Cur = this.logic.constructor; + if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) { + return; + } + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + this.__makeLogic(Next, this.logic.state); + this.__needsDidMount = true; + } + componentDidMount() { + registry.get(this.__name).subs.add(this.__sub); + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } + componentDidUpdate(prevProps) { + this.logic.props = this.__userProps(); + if (this.__needsDidMount) { + if (this.state.__err || !registry.get(this.__name).tpl) return; + this.__needsDidMount = false; + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } else { + try { + this.logic.componentDidUpdate(prevProps); + } catch (e) { + console.error(e); + } + } + } + componentWillUnmount() { + registry.get(this.__name).subs.delete(this.__sub); + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + } + render() { + const r = registry.get(this.__name); + const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); + const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; + const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; + const hostBase = { + className: cls, + style: hostStyle, + "data-sc-name": this.__name, + "data-dc-tpl": this.props.__tplId + }; + const chain = Array.isArray(this.context) ? this.context : []; + if (chain.includes(this.__name)) { + const cycle = [ + ...chain.slice(chain.indexOf(this.__name)), + this.__name + ].join(" \u2192 "); + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: "circular import: " + cycle + }) + ); + } + if (this.state.__err) { + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + this.__name + ": " + this.state.__err + ), + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: this.state.__err + }) + ); + } + this.__reconcileLogic(); + if (!r.tpl) { + return h( + "div", + hostBase, + h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) + ); + } + const userProps = this.__userProps(); + this.logic.props = userProps; + let vals = userProps; + let renderErr = r.logicError || this.__ctorError; + try { + vals = { ...userProps, ...this.logic.renderVals() || {} }; + } catch (e) { + console.error(e); + renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); + } + this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); + this.__htmlStreamingNow = !!r.htmlStreaming; + return h( + "div", + { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, + renderErr && h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + renderErr + ), + h( + AncestorContext.Provider, + { value: [...chain, this.__name] }, + r.tpl(vals, this) + ) + ); + } + } + __publicField(StreamableComponent, "contextType", AncestorContext); + const named = /* @__PURE__ */ new Map(); + function getDC(name) { + const hit = named.get(name); + if (hit) return hit; + function Dispatcher(p) { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + registry.get(name).subs.add(sub); + return () => { + registry.get(name).subs.delete(sub); + }; + }, []); + ensureFetched(name); + return h(StreamableComponent, { ...p, __name: name }); + } + Dispatcher.displayName = name; + named.set(name, Dispatcher); + return Dispatcher; + } + return { + getDC, + StreamableComponent + }; + } + + // src/external.ts + var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); + function isRenderableType(g) { + if (typeof g === "function") return !isElementClass(g); + return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; + } + function resolveDottedPath(root, name) { + let cur = root; + for (const seg of name.split(".")) { + if (cur == null) return void 0; + cur = cur[seg]; + } + return cur; + } + var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"; + var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"; + var GLOBAL_POLL_INTERVAL_MS = 50; + var GLOBAL_POLL_TIMEOUT_MS = 3e4; + function createExternalModules(onResolved) { + const cache = /* @__PURE__ */ new Map(); + let babelLoading = null; + const reportedMissing = /* @__PURE__ */ new Map(); + const polling = /* @__PURE__ */ new Set(); + function ensureBabel() { + if (window.Babel) return Promise.resolve(); + if (babelLoading) return babelLoading; + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = BABEL_URL; + s.integrity = BABEL_SRI; + s.crossOrigin = "anonymous"; + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + const pending = /* @__PURE__ */ new Map(); + function load(kind, url, after) { + const existing = pending.get(url); + if (existing) return existing; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = Promise.all([ + kind === "jsx" ? ensureBabel() : Promise.resolve(), + after ?? Promise.resolve() + ]); + const p = ready.then(() => fetch(url)).then((r) => { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.text(); + }).then((src) => { + const code = kind === "jsx" ? window.Babel.transform(src, { + filename: url, + presets: ["react", "typescript"] + }).code : src; + const module = { exports: {} }; + const before = new Set(Object.keys(window)); + //! nosemgrep: eval-and-function-constructor + new Function("React", "module", "exports", "require", code)( + getReact(), + module, + module.exports, + () => ({}) + ); + const globals = {}; + for (const k of Object.keys(window)) { + if (!before.has(k) && typeof window[k] === "function") { + globals[k] = window[k]; + } + } + cache.set(url, { mod: module.exports, globals }); + console.info( + "[dc-runtime] x-import: loaded", + url, + "\u2014 exports:", + Object.keys(module.exports), + "window globals:", + Object.keys(globals) + ); + onResolved(); + }).catch((e) => { + cache.set(url, { + mod: {}, + globals: {}, + error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) + }); + console.error( + "[dc-runtime] x-import: FAILED to load", + url, + "(" + kind + ")", + e + ); + onResolved(); + }); + pending.set(url, p); + return p; + } + function resolve2(url, name) { + const entry = cache.get(url); + if (!entry) return null; + const { mod, globals } = entry; + const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; + if (typeof C === "function") return C; + const key = url + "\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set( + key, + entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" + ); + console.error( + "[dc-runtime] x-import: module", + url, + "loaded but has no component named", + JSON.stringify(name), + "\u2014 available exports:", + Object.keys(mod), + "window globals:", + Object.keys(globals), + ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." + ); + } + return null; + } + function waitForGlobal(name) { + if (polling.has(name)) return; + polling.add(name); + const started = Date.now(); + const isCE = isCustomElementName(name); + const tick = () => { + const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); + if (found) { + polling.delete(name); + onResolved(); + return; + } + if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { + console.warn( + "[dc-runtime] x-import: global", + JSON.stringify(name), + "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" + ); + return; + } + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + }; + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + } + function resolveGlobal(url, name) { + const isCE = isCustomElementName(name); + if (!url) { + if (isCE) { + if (customElements.get(name)) return name; + waitForGlobal(name); + return null; + } + const g2 = resolveDottedPath(window, name); + if (isRenderableType(g2)) return g2; + waitForGlobal(name); + return null; + } + const entry = cache.get(url); + if (!entry) return null; + if (isCE && customElements.get(name)) return name; + const g = entry.globals[name] ?? resolveDottedPath(window, name); + if (isRenderableType(g)) return g; + if (name.includes(".")) return null; + const key = url + "\0global\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set(key, null); + if (isCE && !customElements.get(name)) { + console.warn( + "[dc-runtime] x-import:", + url, + "loaded but no custom element", + JSON.stringify(name), + "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." + ); + } + } + return name; + } + function getError(url, name) { + const entry = cache.get(url); + if (entry?.error) return entry.error; + return reportedMissing.get(url + "\0" + name) || null; + } + return { load, resolve: resolve2, resolveGlobal, getError }; + } + function isElementClass(g) { + try { + return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; + } catch { + return false; + } + } + + // src/atomics.ts + var ATOMIC_CSS = ( + // layout + ".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}" + ); + + // src/helmet.ts + var DESIGN_DOC_MODE_RE = /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; + var CANVAS_BG_LIGHT = "#f0eee6"; + var CANVAS_BG_DARK = "#2e2c26"; + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + let designDocMode = null; + let canvasStyleEl = null; + let appTheme = "light"; + try { + const ds = doc.documentElement.dataset.theme; + appTheme = ds === "dark" || ds === "light" ? ds : new URLSearchParams(doc.defaultView?.location.search ?? "").get( + "theme" + ) === "dark" ? "dark" : "light"; + } catch { + } + function applyCanvasBg() { + if (!canvasStyleEl) return; + const bg = appTheme === "dark" ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; + canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; + } + function postDesignMode(mode) { + if (window.parent === window) return; + try { + window.parent.postMessage({ type: "__dc_design_mode", mode }, "*"); + } catch { + } + } + function setDesignDocMode(mode) { + if (mode === designDocMode) return; + designDocMode = mode; + postDesignMode(mode); + if (mode === "canvas") { + doc.documentElement.setAttribute("data-dc-canvas", ""); + canvasStyleEl = doc.createElement("style"); + canvasStyleEl.setAttribute("data-dc-canvas", ""); + applyCanvasBg(); + doc.head.appendChild(canvasStyleEl); + } else { + doc.documentElement.removeAttribute("data-dc-canvas"); + canvasStyleEl?.remove(); + canvasStyleEl = null; + } + } + window.addEventListener("message", (e) => { + const type = e.data && e.data.type; + if (type === "__dc_theme") { + const t = e.data.theme; + if (t === "light" || t === "dark") { + appTheme = t; + doc.documentElement.dataset.theme = t; + applyCanvasBg(); + } + return; + } + if (!designDocMode || type !== "__dc_probe") return; + postDesignMode(designDocMode); + }); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) { + mounted.add("__dc-atomics"); + const el = doc.createElement("style"); + el.id = "__dc-atomics"; + el.textContent = ATOMIC_CSS; + doc.head.appendChild(el); + } + return (_vals, ctx) => { + const name = ctx && ctx.__name || ""; + const streaming = !!(name && isStreaming(name)); + for (let i = 0; i < raw.length; i++) { + const child = raw[i]; + const tag = child.tagName; + const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; + if (tag === "SCRIPT") { + if (mayBePartial) continue; + const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); + if (mounted.has(key)) continue; + mounted.add(key); + const el = doc.createElement("script"); + for (const { name: an, value } of [...child.attributes]) + el.setAttribute(an, value); + if (child.textContent) el.textContent = child.textContent; + doc.head.appendChild(el); + } else if (tag === "LINK" || tag === "META") { + if (mayBePartial) continue; + const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); + if (mounted.has(key)) continue; + mounted.add(key); + doc.head.appendChild(child.cloneNode(true)); + } else { + const key = name + "|" + i; + let el = live.get(key); + if (!el || el.tagName !== tag) { + if (el) el.remove(); + el = doc.createElement(tag.toLowerCase()); + live.set(key, el); + doc.head.appendChild(el); + } + for (const { name: an, value } of [...child.attributes]) { + if (el.getAttribute(an) !== value) el.setAttribute(an, value); + } + if (el.textContent !== child.textContent) + el.textContent = child.textContent; + } + } + return null; + }; + } + return { compile, setDesignDocMode }; + } + + // src/pseudo.ts + function createPseudoSheet(doc) { + let el = null; + const cache = /* @__PURE__ */ new Map(); + let n = 0; + return (pseudo, css) => { + const k = pseudo + "|" + css; + const hit = cache.get(k); + if (hit) return hit; + if (!el) { + el = doc.createElement("style"); + doc.head.appendChild(el); + } + const cls = "scp" + (n++).toString(36); + const sel = pseudo === "before" || pseudo === "after" ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule(sel + "{" + css + "}", el.sheet.cssRules.length); + cache.set(k, cls); + return cls; + }; + } + + // src/registry.ts + function createRegistry() { + const entries = /* @__PURE__ */ Object.create(null); + function get(name) { + return entries[name] || (entries[name] = { + html: "", + tpl: null, + Logic: null, + jsStreaming: false, + htmlStreaming: false, + ver: 0, + subs: /* @__PURE__ */ new Set(), + fetched: false + }); + } + function bump(name) { + const r = get(name); + r.ver++; + for (const fn of r.subs) fn(); + } + return { + entries, + get, + bump, + bumpAll() { + for (const n in entries) bump(n); + } + }; + } + + // src/runtime.ts + var COMPONENT_DIR = "."; + function createRuntime(doc = document) { + const registry = createRegistry(); + const pseudoClass = createPseudoSheet(doc); + const helmet = createHelmetManager( + doc, + (name) => registry.get(name).htmlStreaming + ); + const external = createExternalModules(() => registry.bumpAll()); + const factory = createComponentFactory(registry, ensureFetched); + const host = { + component: (name) => factory.getDC(name), + placeholder: (props) => h(Placeholder, props), + helmet: (node) => helmet.compile(node), + loadExternal: (kind, url, after) => external.load(kind, url, after), + resolveExternal: (url, name) => external.resolve(url, name), + resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), + resolveExternalError: (url, name) => external.getError(url, name), + pseudoClass + }; + function ensureFetched(name) { + const r = registry.get(name); + if (r.fetched) return; + r.fetched = true; + const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html"; + fetch(url).then((res) => { + if (!res.ok) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/> failed:", + url, + "returned", + res.status, + "\u2014 the reference renders as an empty placeholder." + ); + return ""; + } + return res.text(); + }).then((t) => { + if (!t) return; + const parsed = parseDcText(t); + if (!parsed) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/>:", + url, + "has no block \u2014 not a Design Component." + ); + return; + } + if (parsed.props) r.propsMeta = parsed.props; + if (parsed.preview) r.preview = parsed.preview; + if (parsed.template && !r.html) updateHtml(name, parsed.template); + if (parsed.js && !r.Logic) updateJs(name, parsed.js); + }).catch( + (e) => console.error( + "[dc-runtime] sibling fetch for <" + name + "/> threw:", + url, + e + ) + ); + } + let rootName = null; + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + if (name === rootName) { + const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; + if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); + } + try { + r.tpl = compileTemplate(html, host); + } catch (e) { + console.error("[dc-runtime] template compile FAILED for", name, e); + } + registry.bump(name); + } + function updateJs(name, src) { + const r = registry.get(name); + const seq = r.jsSeq = (r.jsSeq || 0) + 1; + try { + const Cls = evalDcLogic(src); + if (r.jsSeq !== seq) return; + if (typeof Cls !== "function") { + r.logicError = name + ".dc.html: + + + + + + + + + + +
+
+
+
Vincular padre
a Mateo Fernández
+ +
+
+
Le enviaremos un correo con un código para que active su cuenta. Solo verá el feed de Mateo.
+
NOMBRE DEL PADRE/MADRE
+ +
EMAIL
+ +
PARENTESCO
+
+ + + +
+
+
CÓDIGO DE INVITACIÓN
+
7K4P9
+
Vence en 7 días
+
+ Enviar invitación +
+
+
+ + +
+ + + diff --git a/references/screenshots/compose.png b/references/screenshots/compose.png new file mode 100644 index 0000000..8f24bf6 Binary files /dev/null and b/references/screenshots/compose.png differ diff --git a/references/screenshots/compose2.png b/references/screenshots/compose2.png new file mode 100644 index 0000000..8f24bf6 Binary files /dev/null and b/references/screenshots/compose2.png differ diff --git a/references/screenshots/feed.png b/references/screenshots/feed.png new file mode 100644 index 0000000..9d39683 Binary files /dev/null and b/references/screenshots/feed.png differ diff --git a/references/screenshots/feed2.png b/references/screenshots/feed2.png new file mode 100644 index 0000000..9d39683 Binary files /dev/null and b/references/screenshots/feed2.png differ diff --git a/references/screenshots/ninos.png b/references/screenshots/ninos.png new file mode 100644 index 0000000..c2af5f4 Binary files /dev/null and b/references/screenshots/ninos.png differ diff --git a/references/screenshots/ninos2.png b/references/screenshots/ninos2.png new file mode 100644 index 0000000..d714fe7 Binary files /dev/null and b/references/screenshots/ninos2.png differ diff --git a/references/screenshots/parent-feed.png b/references/screenshots/parent-feed.png new file mode 100644 index 0000000..5f25396 Binary files /dev/null and b/references/screenshots/parent-feed.png differ diff --git a/references/screenshots/post-detail.png b/references/screenshots/post-detail.png new file mode 100644 index 0000000..d3da292 Binary files /dev/null and b/references/screenshots/post-detail.png differ diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..48b5b45 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "skills": { + "spec": { + "source": "klerith/fernando-skills", + "sourceType": "github", + "skillPath": "skills/engineering/spec/SKILL.md", + "computedHash": "81cfa08325e8104fbf5868ec47e3f3d8aec5f18927f58178fbceeecdc0036e55" + }, + "spec-impl": { + "source": "klerith/fernando-skills", + "sourceType": "github", + "skillPath": "skills/engineering/spec-impl/SKILL.md", + "computedHash": "18493640ac83a2f3baab75366eba5eab6acabdd893cc63e533a10629e4844ed9" + } + } +}