From 51682a901d08215c9238246ac5f160f4a2bee275 Mon Sep 17 00:00:00 2001 From: ayushya Date: Thu, 24 Sep 2026 14:56:47 -0400 Subject: [PATCH] Add design system spec for the upcoming UI rework Moved into .claude/context/ (alongside the other topic docs CLAUDE.md points agents at) rather than the repo root, so it's discovered the same way as architecture.md/styling.md/etc. Flags styling.md as documenting the pre-rework theme in the meantime, so the two don't read as contradictory. Co-Authored-By: Claude Sonnet 5 --- .claude/context/design-system/.thumbnail | Bin 0 -> 27562 bytes .../context/design-system/DESIGN_SYSTEM.md | 305 +++ .../design-system/Desktop Screen.dc.html | 340 +++ .../design-system/Mobile Screen.dc.html | 433 ++++ .../design-system/Noo Design System.dc.html | 202 ++ .../context/design-system/Noo Screens.dc.html | 93 + .claude/context/design-system/noo-kit.js | 43 + .claude/context/design-system/support.js | 1911 +++++++++++++++++ .claude/context/styling.md | 14 +- CLAUDE.md | 5 + 10 files changed, 3345 insertions(+), 1 deletion(-) create mode 100644 .claude/context/design-system/.thumbnail create mode 100644 .claude/context/design-system/DESIGN_SYSTEM.md create mode 100644 .claude/context/design-system/Desktop Screen.dc.html create mode 100644 .claude/context/design-system/Mobile Screen.dc.html create mode 100644 .claude/context/design-system/Noo Design System.dc.html create mode 100644 .claude/context/design-system/Noo Screens.dc.html create mode 100644 .claude/context/design-system/noo-kit.js create mode 100644 .claude/context/design-system/support.js diff --git a/.claude/context/design-system/.thumbnail b/.claude/context/design-system/.thumbnail new file mode 100644 index 0000000000000000000000000000000000000000..5279edd0cc652f24ce42d80f19caae22cce68cdc GIT binary patch literal 27562 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 literal 0 HcmV?d00001 diff --git a/.claude/context/design-system/DESIGN_SYSTEM.md b/.claude/context/design-system/DESIGN_SYSTEM.md new file mode 100644 index 0000000..f70aeae --- /dev/null +++ b/.claude/context/design-system/DESIGN_SYSTEM.md @@ -0,0 +1,305 @@ +# Noo Design System + +Noo is a Nextcloud client for iOS, Android, macOS and Windows. ("Noo" is a placeholder name.) +Its visual language is warm neutrals with one violet accent, flat surfaces, pill-shaped controls, and no gradients. + +**Platform rule:** keep one visual language everywhere. Adapt only the navigation chrome (status bar, top bar, bottom bar, window controls) to each platform. Content components look the same on every platform. + +Reference files in this project: +- `Noo Screens.dc.html` has every screen on every platform, in light and dark. +- `Noo Design System.dc.html` is the visual component sheet. +- `noo-kit.js` holds the tokens (`TH`), the icon set (`SVG`/`ic`), file-type mapping (`KIND`) and mock data. +- `Mobile Screen.dc.html` and `Desktop Screen.dc.html` are the reference builds for each screen. + +--- + +## 1. Tokens + +### 1.1 Color (semantic). Use these names in code. + +| Token | Role | Light | Dark | +|---|---|---|---| +| `bg` | App background, desktop sidebar | `#F3EFE6` warm off-white | `#0E0D0B` | +| `surface` (`sf`) | Cards, rows, bars, sheets, desktop main pane | `#FFFFFF` | `#25241F` | +| `surface-2` (`sf2`) | Inputs, inset panels, secondary buttons | `#F6F5F2` | `#3D3C38` | +| `surface-3` (`sf3`) | Progress tracks, toggle off, grabbers | `#EAE8E2` | `#5A5852` | +| `line` (`ln`) | Dividers, 1px borders | `#EAE8E2` | `#3D3C38` | +| `fg-1` | Primary text | `#0E0D0B` | `#F3EFE6` | +| `fg-2` | Secondary text, idle icons | `#5A5852` | `#BDBAB1` | +| `fg-3` | Meta text, placeholders, trailing icons | `#7C7A72` | `#9E9B92` | +| `accent` (`ac`) | Primary fills (buttons, FAB, toggle on, progress) | `#8D0DE3` | `#8D0DE3` | +| `accent-text` (`act`) | Accent text and icons, active nav | `#8D0DE3` | `#CBA5FD` | +| `accent-soft` (`acs`) | Selected and active backgrounds, tonal buttons | `#ECDFFF` | `#2F0454` | +| `danger` / `danger-soft` | Destructive text / bg | `#A8202A` / `#FDE7E7` | `#F49E9E` / `#560F18` | +| `danger-fill` | Swipe-delete background (white text) | `#A8202A` | `#A8202A` | +| `success` / `success-soft` | Synced, connected | `#3E7A23` / `#EAF5DF` | `#B7DD9F` / `#1E3F10` | +| `warning` / `warning-soft` | Video file type | `#8C5A0E` / `#FBEFD0` | `#F0C26C` / `#4A3208` | +| `info` / `info-soft` | Docs, images | `#2940A8` / `#E6ECFE` | `#B6C3FB` / `#1C2B73` | +| `scrim` | Behind sheets, drawers, dialogs | `rgba(14,13,11,.45)` | `rgba(0,0,0,.6)` | + +Rules: +- Text on `accent` is always `#FFFFFF`. Accent-colored text uses `accent-text`, never `accent` in dark mode. +- On-color text always uses the matching pair: `accent-text` on `accent-soft`, `danger` on `danger-soft`, and so on. +- Don't use gradients. Don't add new colors. Avatar and photo placeholder fills use the secondary palette: `#8DCDE2 #96B0FD #E5D8BD #EEEDB3 #DA9AC7 #A7D296 #E9A6A7 #85D1BD #CBA5FD`, always with `#0E0D0B` text. + +### 1.2 File-type tiles (`KIND`) +| Kind | Background | Icon color | Lucide icon | +|---|---|---|---| +| folder | accent-soft | accent-text | `folder` | +| pdf | danger-soft | danger | `file-text` | +| doc / text | info-soft | info | `file-text` | +| sheet | success-soft | success | `sheet` | +| video | warning-soft | warning | `film` | +| image | info-soft | info | `image` | +| archive | surface-3 | fg-2 | `file-archive` | + +Tile sizes: 40px/r12 for mobile rows, 30px/r9 for desktop rows, 44px/r12 for the share header, 32px/r10 for activity. The icon is half the tile size (20, 16, 24, 16). + +### 1.3 Typography +Fonts (Google Fonts): **Schibsted Grotesk** for headings (weights 500/600) and **Instrument Sans** for UI (weights 400/500/600). Load them with `https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600&family=Schibsted+Grotesk:wght@500;600&display=swap`. Don't use italics, all-caps or wide tracking. + +| Style | Font | Size / line-height | Tracking | Use | +|---|---|---|---|---| +| Large title | Schibsted 600 | 34 / 0.9 | −3% | iOS large title | +| Stat | Schibsted 600 | 32 (mobile) · 28 (desktop) / 0.9 | −3% | Offline GB, counters | +| Title | Schibsted 500 | 22 / 1 | −2% | Android top bar, desktop toolbar | +| Card title | Schibsted 500 | 17–20 / 1 | −2% | Settings cards, sheet sections, account name | +| Group heading | Schibsted 500 | 18 / 1 | −2% | Photos month | +| Body L | Instrument 400/500 | 16 / 1.2 | 0 | Mobile row title, settings label | +| Body | Instrument 400/500 | 14–15 / 1.3 | 0 | Desktop rows, activity text, banners | +| Label | Instrument 600 | 13 / 1 | 0 | Section labels (fg-2), chips, table headers (12, fg-3) | +| Meta | Instrument 400 | 13 (mobile) · 12–13 (desktop) / 1.2 | 0 | Size · date, subtitles (fg-3) | +| Nav label | Instrument 500 / 600 (active) | 12 Android · 10 iOS | 0 | Bottom bars | +| Button | Instrument 600 | 14–16 / 1 | 0 | All buttons | +| Mono | ui-monospace 400 | 13–14 | 0 | Share URLs, local paths | + +### 1.4 Space, radius, elevation, motion +- **Spacing** uses a 4px base: 4, 8, 12, 14, 16, 20, 24, 32. Mobile screen gutter is **12px** (cards) and 16px (bars). Desktop content gutter is **24px**. +- **Radius:** + - pills (`999px`) for every button, chip, segmented control, toggle, search field and badge + - 20px for cards and grouped lists + - 18px for grid cards and stat cards + - 14px for inputs and inset panels + - 12px for desktop sidebar items and selected table rows + - 28px for the sheet top and drawer edge + - 24px for desktop dialogs + - screen corners are 48 on iOS and 36 on Android +- **Elevation:** product UI is flat, with no shadows on buttons or cards. The only shadow is `0 24px 48px rgba(30,0,47,.14)`, and only on floating desktop dialogs. Separate things by surface color, not by shadow. +- **Motion:** use `cubic-bezier(.7,0,.15,1)` with durations of 180, 280 and 480ms. Nothing bounces. Pressing a button scales it to 0.98. + +### 1.5 Icons +Use **Lucide** (pinned to `lucide-static@0.460.0`) at a 1.8 stroke. Sizes are 14, 16, 18, 20, 24 and 32. Icons render in the current text color. Tab icons: Files `folder`, Photos `images`, Favorites `star`, Offline `hard-drive-download`, Shares `share-2`, Recent `clock`, Activity `activity`, Trash `trash-2`, Settings `settings`. + +--- + +## 2. Components + +**Button** +- Always a pill. Heights: 52 (sheet CTA), 44 (mobile card), 40 (inline in a field), 36 (desktop toolbar), 28–32 (compact). +- Variants: + - *Primary*: accent fill, white text. + - *Tonal*: accent-soft fill, accent-text. + - *Secondary*: surface-2 fill, fg-1. + - *Danger*: danger-soft fill, danger text. + - *Outline*: 1.5px surface-3 border. +- An icon goes on the left, with a 6–10px gap. + +**FAB** (Android only) +- An extended pill: 56px tall, accent fill, labelled "Upload". +- Sits 16px from the right and bottom edges of the content area. +- Shows only on Files and Photos. + +**Chip** +- A pill, 32–34px tall, 12px horizontal padding, Label 13–14. +- Idle: surface fill on mobile, or a 1px line border on desktop. +- Selected: accent-soft fill with accent-text. +- A trailing `chevron-down` means it opens a menu. A trailing `x` means it's an active filter. + +**Segmented control** +- A pill track (surface or surface-2) with 3px padding. +- The active segment gets an accent-soft fill with accent-text at weight 600. Idle segments use fg-2 at weight 500. +- Use it for the List/Grid toggle (icons only, 36×28), Shares scope, Theme, and default view. + +**Toggle** +- 48×28 on mobile, 44×26 on desktop, with a white knob 3px inset. +- On: accent track. Off: surface-3 track. It looks the same on every platform. + +**Search field** +- A pill (radius 12 on the iOS inline field), 36–38px tall. +- Surface fill on bg, or surface-2 fill on surface. Leading search icon and placeholder in fg-3. +- Placement: + - iOS: below the large title. + - Android: an icon in the top bar that expands. + - Desktop: 260px wide in the toolbar. + +**Grouped list (card)** +- A surface card with radius 20 and 1px gaps showing `line`, which draws the dividers. +- An optional group label above it: 13/600 in fg-2, inset 8px. +- Each group is 18px from the next. + +**File row** +- Mobile: 64px tall. + - Left to right: tile 40, then a column with the name (16/500, one line, ellipsis) above a meta line (13, fg-3), then optional trailing icons, then the overflow menu (`ellipsis` on iOS, `ellipsis-vertical` on Android). + - The meta line is `[status icon][shared icon] size · modified`. + - A favorite shows an accent-text star before the overflow menu. + - Trailing icon by screen: star in Favorites, `rotate-ccw` in Trash. +- Desktop: a table row 52px tall. + - Columns are `minmax(0,1fr) 180px 160px 120px`: Name, two data columns, then status icons and the overflow menu. + - The header row is 36px, 12/600 in fg-3, with a sort arrow on the active column. + - A selected row gets an accent-soft fill with radius 12. + +**Status icons (14px)** +- Synced: `circle-check` in success. +- Syncing: `refresh-cw` in accent-text. +- Error: `circle-alert` in danger, and the meta text says what to do ("Couldn't sync · Tap to retry"). +- Shared: `users` in fg-3. + +**Swipe action** +- The row slides and uncovers a 96px action block. Delete is white on danger-fill; Favorite is white on accent. +- The block has a 20px icon above a 12/600 label. +- Swipe actions are set in Settings → Swipe on a file. + +**Grid card** +- Surface fill, radius 18. Mobile uses 2 columns with a 10px gap; desktop uses 5 columns with a 16px gap. +- The thumbnail area is 104–118px tall, filled with the file-type soft color and a 32px icon. Real thumbnails replace it when they exist. +- Below it: the name (14/500) with an overflow menu, then the meta (12). + +**Photo grid** +- Square tiles with a 2px gap on mobile (3 columns, edge to edge) and a 4px gap on desktop (8 columns). +- Tiles are grouped by month, with the group heading on the left and the count on the right. +- Videos get a badge in the bottom-right: a `play` icon and the duration on `rgba(14,13,11,.7)`. + +**Activity item** +- Avatar 36 (initials on a secondary-palette color), then a sentence: **Actor** verb **object** tail. +- Time in meta style. The file tile (32) goes on the right. + +**Banner / summary card** +- A surface card with radius 20 (surface-2 on desktop) and padding of 14–18. +- Trash: an info icon, the retention text, and "Empty trash" as a danger text button. +- Offline: the stat, a 6px progress bar, the last-sync meta, and a "Sync now" tonal button. + +**Progress bar** +- 6px tall pill. Surface-3 track with an accent fill. +- Used for storage quota, cache use and the offline limit. + +**Avatar** +- A circle with initials, weight 600, at 0.36× the avatar size. +- Sizes: 30–32 in bars, 36 in lists, 48 in the drawer, 52–56 on the account card. +- The current user gets an accent-soft fill with accent-text. + +**Settings row** +- 52px tall, or 60px with a subtitle. +- Left to right: an optional 20px icon in fg-2, then the label (16) with an optional subtitle (13, fg-3), then a trailing control. +- Trailing control is one of: a toggle, a value in fg-3 with a chevron (desktop uses a 32px dropdown with radius 10), a segmented control, a pill button, or a status badge. +- A destructive row uses danger text. + +**Tab order row** +- A grip icon (`grip-vertical`, fg-3), the tab icon, the label, then a pin button: a 36px circle (30 on desktop) that is accent-soft with `pin` when pinned, or surface-2 with `pin-off` when not. +- Allow at most 5 pinned tabs. + +**Sheet** (mobile) +- Surface fill, radius 28 at the top. A 36×5 grabber in surface-3. Padding 20, with 22px between sections. +- It sits on a scrim. + +**Dialog** (desktop) +- 540px wide, radius 24, padding 24, sitting on a scrim. +- Header: file tile, title "Share """, and a close button (32px circle, surface-2). + +**Drawer** (mobile) +- 316px wide, surface fill, radius 28 on the trailing edge, on a scrim. +- Top to bottom: + - account block with a chevron to switch accounts + - storage bar + - "More" label, then the unpinned tabs as 52px pill items (icon 24, label 16/500, trailing count) + - Settings + - an "Edit tabs" link at the bottom, in accent-text + +--- + +## 3. Navigation & platform mapping + +**Tabs** (the user sets the order): Files, Photos, Favorites, Offline, Shares, Recent, Activity, Trash. Settings is always separate. + +### Mobile + +Mobile always has **5 pinned tabs in the bottom bar**. The rest go in the drawer, which opens from the top-left menu icon. + +| Element | iOS | Android | +|---|---|---| +| Status bar | 54px, Dynamic Island | 40px | +| Top bar | 44px row: `menu` on the left; `plus` and avatar on the right (all accent-text). Then a 34px large title, then the search field. | 64px: `menu`, 22px title, `search`, avatar | +| Bottom bar | Surface fill, 1px top line. Icon 24 above a 10px label. Active: accent-text. Idle: fg-3. 34px home indicator below. | Surface fill, 80px. The active icon sits in a 56×32 accent-soft pill with a 12px label. 20px gesture bar below. | +| Upload | `plus` in the nav bar | Extended FAB | +| Overflow icon | `ellipsis` | `ellipsis-vertical` | +| Settings | Pushed screen with a "‹ Files" back button and a large title. No bottom bar. | Pushed screen with a back arrow and a title. No bottom bar. | +| Biometric label | "Lock with Face ID" | "Lock with fingerprint" | + +### Desktop + +Desktop has a 256px sidebar in `bg` and a main pane in `surface`, separated by a 1px line. + +**Sidebar**, top to bottom: +- account switcher card +- the 5 pinned tabs +- a divider +- the remaining tabs +- a spacer +- the storage meter +- Settings + +Sidebar items are 38px tall with radius 12, an 18px icon and a 14/500 label. The active item gets an accent-soft fill with accent-text at weight 600. Counts sit on the right: Trash 12, Offline errors, new shares. + +**Toolbar** is 64px tall: +- the title (22px Schibsted Grotesk), with a breadcrumb in Files +- a spacer +- the search field +- context actions: List/Grid toggle and Upload in Files, Upload in Photos, Empty trash in Trash, Sync now in Offline + +| Element | macOS | Windows | +|---|---|---| +| Window | Radius 12 | Radius 8 | +| Controls | Traffic lights at the top of the sidebar | 40px title bar with the app mark and name; minimize, maximize and close buttons 46px wide | +| Navigation | Back and forward chevrons at the start of the toolbar | Not shown in the toolbar | +| Biometric | "Touch ID" | "Windows Hello" | +| Local path | `~/Noo/…` | `C:\Users\…\Noo` | +| Swipe setting | "Trackpad swipe on a file" | "Touch swipe on a file" | + +--- + +## 4. Screen recipes + +- **Files:** + - Toolbar: sort chip (`Name ↑`), type filter chip, List/Grid segmented toggle. + - Desktop adds Modified and Shared filter chips, an active-filter chip, and an item count. + - Content: a list, or a grid when the toggle is set. +- **Photos:** type chips (All, Photos, Videos, Camera), then the photo grid grouped by month. The grid view is the only view. +- **Favorites:** a file list with a star as the trailing icon. +- **Offline:** a summary card (or 4 stat cards on desktop), then a list whose rows show sync status icons and status meta. +- **Shares:** a segmented control (With you, By you, Links), then a list. The meta reads "Owner · Permission". +- **Recent:** a list grouped into Today, Yesterday and This week. The meta is the action plus the location. +- **Activity:** the feed grouped by day. Desktop limits it to 760px wide. +- **Trash:** a retention banner, then a list. Mobile rows get a restore icon; desktop rows get a tonal "Restore" pill. +- **Settings:** sections in this order: + 1. Account card + 2. Accounts (list, then "Add account") + 3. Security + 4. File sync + 5. Files cache + 6. Appearance + 7. Tabs + 8. Swipe on a file + + Mobile uses one column of grouped lists. Desktop uses a 2-column grid of cards with a 1px line and radius 20. +- **Share sheet / dialog:** sections in this order: + 1. Header: file tile, name, size · folder, and close. + 2. **Share with people:** an input ("Name, email or group"), then the people with access. The owner comes first; the others each have a permission pill ("Can edit ▾"). + 3. **Share link:** a toggle, then the URL field in mono with a primary "Copy link" button, then option chips (permission, expiry, password, allow download). + 4. **Send file directly:** an outline button, with a caption saying that link settings don't apply. + +--- + +## 5. Copy +- Use plain, active and short sentences, under 15 words. Say "you". +- Name the problem and the fix: "Couldn't sync · Tap to retry", not "An error occurred". +- Use sentence case everywhere and no all-caps. Use a middle dot ( · ) to separate meta values. +- Don't use emoji in the product UI. diff --git a/.claude/context/design-system/Desktop Screen.dc.html b/.claude/context/design-system/Desktop Screen.dc.html new file mode 100644 index 0000000..e41826a --- /dev/null +++ b/.claude/context/design-system/Desktop Screen.dc.html @@ -0,0 +1,340 @@ + + + + + + + + + + + + + + + +
+ + +
+
Noo
+
+
+
+
+
+ +
+
+ +
+
+
+
MR
+
Marina Rocha
cloud.rocha.family
+ +
+ +
{{ n.label }}
{{ n.count }}
+
{{ n.label }}
{{ n.count }}
+
+
+ +
{{ n.label }}
{{ n.count }}
+
{{ n.label }}
{{ n.count }}
+
+
+
+
Storage38.2 of 100 GB
+
+
+
Settings
+
Settings
+
+ +
+
+
+
{{ title }}All files
+
+
Search in {{ title }}
+ +
+
Upload
+
+
Upload
+
Empty trash
+
Sync now
+
+ +
+ + +
+
All types
+
Modified
+
Shared
+
Available offline
+
+
10 items · 440 MB
+
+
+ +
Items here are deleted for good after 30 days.
+
+ +
+
2.4 GB
of 5 GB limit
+
142
Files up to date
+
6
Syncing now
+
1
Needs attention
+
+
+ +
+
With you
+
By you
+
Links
+
+
+ + + +
{{ g.label }}
+
+
{{ c0 }}
{{ c1 }}
{{ c2 }}
+
+ +
+
{{ f.name }}
+
{{ f.a }}
+
{{ f.b }}
+
+ + + +
Restore
+ +
+
+
+
+
+ + +
+ +
+
+
+
{{ f.name }}
+
{{ f.meta }}
+
+
+
+
+
+ + +
+
All
+
Photos
+
Videos
+
Camera uploads
+
+ +
{{ g.label }}
{{ g.count }}
+
+ +
{{ t.dur }}
+
+
+
+
+ + +
+ +
{{ g.label }}
+ +
+
{{ a.who }}
+
{{ a.name }} {{ a.verb }} {{ a.obj }} {{ a.tail }}
+
+
{{ a.time }}
+
+
+
+
+
+ + +
+
+
+
MR
Marina Rocha
marina@rocha.family
Connected
+
Servercloud.rocha.familyVersionNextcloud 30.0.2Storage
38.2 of 100 GB
+
Manage on web
Log out
+
+
+
Accounts
+
MR
Marina Rocha
cloud.rocha.family
+
MR
Marina · Work
files.acme-studio.com
Switch
+
Add account
+
+
+
+
+
Security
+
{{ bioLabel }}
Needed every time you open Noo
+
Lock after
5 minutes idle
+
+
+
File sync
+
Local sync folder
{{ syncPath }}
Change
+
Sync automatically
+
Download files on demand
+
Pause on metered networks
+
+
+
+
+ + +
+
+
+
Files cache
+
Cache in use1.2 of 2 GB
+
Cache limit
2 GB
+
Clear cached files
Clear cache
+
+
+
Appearance
+
Theme
System
Light
Dark
+
Default file view
List
Grid
+
Show hidden files
+
+
+
+
+
Sidebar tabs
Drag to reorder
+
Pinned tabs show at the top of the sidebar, and in the bottom bar on mobile.
+ +
{{ t.label }}
+
+
+
+
{{ swipeTitle }}
+
Swipe left
Delete
+
Swipe right
Favorite
+
+
+
+
+ +
+ + +
+
+
+
+
Share “Q3 Planning.pdf”
2.4 MB · Documents
+
+
+
+
Share with people
+
Name, email or group
Can edit
+
MR
You
marina@rocha.family
Owner
+
AS
Ana Souza
ana@rocha.family
Can edit
+
Design team
Group · 6 people
Can view
+
+
+
+
Share link
Anyone with the link can view
+
https://cloud.rocha.family/s/k7Pq2xW
Copy link
+
+
Can view
+
Expires Oct 24
+
Add password
+
Allow download
+
+
+
+
+
Sends a copy through another app. Link settings don’t apply.
+
Send file directly
+
+
+
+
+
+
+
+
+
+ + + diff --git a/.claude/context/design-system/Mobile Screen.dc.html b/.claude/context/design-system/Mobile Screen.dc.html new file mode 100644 index 0000000..88f6984 --- /dev/null +++ b/.claude/context/design-system/Mobile Screen.dc.html @@ -0,0 +1,433 @@ + + + + + + + + + + + + + + + +
+ + +
+9:41 +
+
+
+
+ +
+9:41 +
+
+
+ + +
+
+
{{ title }}
+
+
MR
+
+
+ +
+
+
Settings
+
+
+ +
+ +
MR
+
+
{{ title }}
+
Search
+
+ +
Files
+
Settings
+
+ +
+ + +
+
Name
+
All types
+
+
+
+
+
+
+
+ + +
+ +
Items here are deleted for good after 30 days.
+
Empty trash
+
+
+ + +
+
+
2.4 GB
of your 5 GB offline limit
+
Sync now
+
+
+
Last synced 2 minutes ago · 1 file needs attention
+
+
+ + +
+
With you
+
By you
+
Links
+
+
+ + +
+ +
+
{{ g.label }}
+
+ +
+
Delete
+
+
+
+
{{ f.name }}
+
+ + +{{ f.meta }} +
+
+ +
+
+
+
+
+
+
+
+
+ + +
+ +
+
+
+
{{ f.name }}
+
{{ f.size }}
+
+
+
+
+
+ + +
+
All
+
Photos
+
Videos
+
Camera
+
+ +
{{ g.label }}
{{ g.count }}
+
+ +
{{ t.dur }}
+
+
+
+
+ + +
+ +
+
{{ g.label }}
+
+ +
+
{{ a.who }}
+
+
{{ a.name }} {{ a.verb }} {{ a.obj }} {{ a.tail }}
+
{{ a.time }}
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
MR
+
Marina Rocha
marina@rocha.family
+
+
+ +
cloud.rocha.family
Nextcloud 30.0.2
+
Connected
+
+
+
Storage38.2 of 100 GB
+
+
+
+
Manage on web
+
Log out
+
+
+
+
Accounts
+
+
MR
Marina Rocha
cloud.rocha.family
+
MR
Marina · Work
files.acme-studio.com
+
Add account
+
+
+
+
Security
+
+
{{ bioLabel }}
Needed every time you open the app
+
Lock after
1 minute
+
+
+
+
+ + +
+
+
File sync
+
+
Auto-upload photos
To /Photos/Camera
+
Wi-Fi only
+
Only while charging
+
Refresh offline files
Every 15 min
+
+
+
+
Files cache
+
+
Cache in use1.2 of 2 GB
+
Cache limit
2 GB
+
Clear cache
+
+
+
+
Appearance
+
+
Theme
System
Light
Dark
+
Default view
List
Grid
+
Show hidden files
+
+
+
+
+ + +
+
+
Tabs5 of 5 pinned
+
+ +
+ + +
{{ t.label }}
+
+
+
+
+
Pinned tabs sit in the bottom bar. The rest live in the menu. Drag to reorder.
+
+
+
Swipe on a file
+
+
Swipe left
Delete
+
Swipe right
Favorite
+
+
+
+
+ + +
Upload
+
+
+ + +
+
+ +
+
{{ n.label }}
+
{{ n.label }}
+
+
+
+
+
+
+ +
+
+ +
{{ n.label }}
+
{{ n.label }}
+
+
+
+
+
+
+
+ + +
+
+
+
MR
+
Marina Rocha
cloud.rocha.family
+
+
+
+
+
38.2 GB of 100 GB used
+
+
+
More
+ +
{{ m.label }}
{{ m.count }}
+
+
+
Settings
+
+
Edit tabs
+
+
+ + +
+
+
+
+
+
Q3 Planning.pdf
2.4 MB · Documents
+
+
+
+
Share with people
+
Name, email or group
+
+
MR
You
marina@rocha.family
Owner
+
AS
Ana Souza
ana@rocha.family
Can edit
+
Design team
Group · 6 people
Can view
+
+
+
+
+
Share link
Anyone with the link can view
+
cloud.rocha.family/s/k7Pq2xW
Copy link
+
+
Can view
+
Expires Oct 24
+
Add password
+
+
+
+
Send file directly
+
Sends a copy through another app. Link settings don’t apply.
+
+
+
+ +
+
+
+ + + diff --git a/.claude/context/design-system/Noo Design System.dc.html b/.claude/context/design-system/Noo Design System.dc.html new file mode 100644 index 0000000..a9773eb --- /dev/null +++ b/.claude/context/design-system/Noo Design System.dc.html @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + +
+
+
Noo design system
+
Every component is shown in light and dark. The full rules for code are in DESIGN_SYSTEM.md. You can see every screen in Noo Screens.
+
+ + +
+
Type
+
+ +
+
{{ t.spec }}
+
{{ t.sample }}
+
+
+
+
+ +
+ +
+
+
{{ th.label }}
+ +
+
Color
+
+ +
+
+
{{ c.name }}
{{ c.hex }}
+
+
+
+
+ +
+
File-type tiles
+
+ +
{{ k.name }}
+
+
+
+ +
+
Buttons
+
+
Primary
+
Tonal
+
Secondary
+
Danger
+
Outline
+
FAB · Android
+
+
+ +
+
Chips, segmented, toggles, search
+
+
Name
+
Modified
+
Available offline
+
+
With you
By you
Links
+
+
+
+
Search
+
+ +
+
Grouped list · file rows · swipe
+
+ +
+
Delete
+
+
+
+
{{ f.name }}
+
{{ f.meta }}
+
+ +
+
+
+
+
+
+ +
+
Settings rows · progress · badges · avatars
+
+
Toggle row
With a subtitle
+
Value row
1 minute
+
Progress row1.2 of 2 GB
+
Destructive row
+
+
+
Connected
+
Couldn’t sync
+
Synced
+
Syncing
+
Error
+
+
MR
+
AS
+
LP
+
+
+ +
+
Navigation · iOS tab bar / Android navigation bar / desktop sidebar
+
+ +
{{ n.label }}
+
{{ n.label }}
+
+
+
+ +
+
{{ n.label }}
+
{{ n.label }}
+
+
+
+
+
Files
+
Trash
12
+
+
+ +
+
+
+
+
+
+
+ + + diff --git a/.claude/context/design-system/Noo Screens.dc.html b/.claude/context/design-system/Noo Screens.dc.html new file mode 100644 index 0000000..cbff552 --- /dev/null +++ b/.claude/context/design-system/Noo Screens.dc.html @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + +
+
+
Noo
+
A Nextcloud client for iOS, Android, macOS and Windows. One brand language, navigation adapted per platform. Light and dark shown for every screen.
+ +
+ + +
+
{{ p.name }}
{{ p.note }}
+
+ +
+
{{ t.label }}
+
+ +
+
{{ sc.n }}{{ sc.label }}
+
+ +
+
+
+
+
+
+
+
+
+ + +
+
{{ p.name }}
{{ p.note }}
+
+ +
+
{{ t.label }}
+
+ +
+
{{ sc.n }}{{ sc.label }}
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/.claude/context/design-system/noo-kit.js b/.claude/context/design-system/noo-kit.js new file mode 100644 index 0000000..8ef104c --- /dev/null +++ b/.claude/context/design-system/noo-kit.js @@ -0,0 +1,43 @@ +// Shared tokens, icons and mock data for the Noo (Nextcloud client) mocks. +(function () { + // Icons: Lucide (lucide-static@0.460.0), inlined as mask data URIs. + const SVG = {"menu":"","search":"","plus":"","x":"","check":"","chevR":"","chevD":"","chevL":"","back":"","sort":"","up":"","filter":"","list":"","grid":"","folder":"","doc":"","sheet":"","film":"","image":"","zip":"","star":"","moreV":"","moreH":"","ok":"","sync":"","alert":"","photos":"","offline":"","share":"","clock":"","activity":"","trash":"","settings":"","restore":"","link":"","copy":"","userPlus":"","users":"","lock":"","fingerprint":"","face":"","calendar":"","send":"","grip":"","pin":"","pinOff":"","info":"","logout":"","server":"","wifi":"","signal":"","battery":"","charging":"","camera":"","database":"","palette":"","swipe":"","upload":"","play":"","pencil":"","timer":"","minus":"","square":"","panel":"","download":"","hidden":"","eye":"","external":"","key":"","monitor":"","plusCircle":""}; + const ic = (n, s, c = 'currentColor') => { const u = `url("data:image/svg+xml,${encodeURIComponent(SVG[n])}") center/contain no-repeat`; return { width: s, height: s, flex: 'none', display: 'block', background: c, WebkitMask: u, mask: u }; }; + const set = s => Object.fromEntries(Object.keys(SVG).map(k => [k, ic(k, s)])); + const I = { 14: set(14), 16: set(16), 18: set(18), 20: set(20), 24: set(24), 32: set(32) }; + + const FT = "'Instrument Sans', system-ui, sans-serif", FD = "'Schibsted Grotesk', system-ui, sans-serif"; + const TH = { + light: {'--bg':'#F3EFE6','--sf':'#FFFFFF','--sf2':'#F6F5F2','--sf3':'#EAE8E2','--ln':'#EAE8E2','--fg1':'#0E0D0B','--fg2':'#5A5852','--fg3':'#7C7A72','--ac':'#8D0DE3','--act':'#8D0DE3','--acs':'#ECDFFF','--dg':'#A8202A','--dgs':'#FDE7E7','--dgf':'#A8202A','--ok':'#3E7A23','--oks':'#EAF5DF','--wn':'#8C5A0E','--wns':'#FBEFD0','--in':'#2940A8','--ins':'#E6ECFE','--scrim':'rgba(14,13,11,0.45)','--ft':FT,'--fd':FD}, + dark: {'--bg':'#0E0D0B','--sf':'#25241F','--sf2':'#3D3C38','--sf3':'#5A5852','--ln':'#3D3C38','--fg1':'#F3EFE6','--fg2':'#BDBAB1','--fg3':'#9E9B92','--ac':'#8D0DE3','--act':'#CBA5FD','--acs':'#2F0454','--dg':'#F49E9E','--dgs':'#560F18','--dgf':'#A8202A','--ok':'#B7DD9F','--oks':'#1E3F10','--wn':'#F0C26C','--wns':'#4A3208','--in':'#B6C3FB','--ins':'#1C2B73','--scrim':'rgba(0,0,0,0.6)','--ft':FT,'--fd':FD} + }; + const KIND = {folder:['--acs','--act','folder'],pdf:['--dgs','--dg','doc'],doc:['--ins','--in','doc'],sheet:['--oks','--ok','sheet'],video:['--wns','--wn','film'],image:['--ins','--in','image'],zip:['--sf3','--fg2','zip']}; + const tile = (k, s, r, bg) => ({ width: s, height: s, borderRadius: r, flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', background: `var(${KIND[k][0]})`, color: `var(${KIND[k][1]})` }); + const BADGE = { ok: ic('ok', 14, 'var(--ok)'), sync: ic('sync', 14, 'var(--act)'), err: ic('alert', 14, 'var(--dg)') }; + const F = (name, kind, size, mod, o = {}) => ({ name, kind, size, mod, ...o }); + + const D = { + files: [F('Documents','folder','24 items','Sep 20'),F('Photos','folder','1,204 items','Today'),F('Projects','folder','8 items','Sep 19',{shared:1}),F('Q3 Planning.pdf','pdf','2.4 MB','Yesterday',{fav:1,off:'ok'}),F('Budget 2026.xlsx','sheet','186 KB','Sep 18',{shared:1,swiped:1}),F('Offsite recap.mp4','video','412 MB','Sep 12',{off:'ok'}),F('Brand brief.docx','doc','58 KB','Sep 10'),F('Whiteboard.jpg','image','3.1 MB','Sep 9'),F('Invoices 2026.zip','zip','24 MB','Sep 2'),F('Lease 2026.pdf','pdf','880 KB','Aug 28')], + favorites: [F('Q3 Planning.pdf','pdf','2.4 MB','Yesterday',{off:'ok'}),F('Projects','folder','8 items','Sep 19',{shared:1}),F('Passport scan.pdf','pdf','1.1 MB','Aug 30',{off:'ok'}),F('Family budget.xlsx','sheet','92 KB','Aug 21',{shared:1}),F('Wedding','folder','312 items','Jun 14'),F('Tax return 2025.pdf','pdf','640 KB','Apr 2')], + offline: [F('Q3 Planning.pdf','pdf','2.4 MB','',{off:'ok',meta:'Up to date · 2.4 MB'}),F('Offsite recap.mp4','video','412 MB','',{off:'ok',meta:'Up to date · 412 MB'}),F('Contracts','folder','18 items','',{off:'sync',meta:'Syncing 12 of 18 files'}),F('Passport scan.pdf','pdf','1.1 MB','',{off:'ok',meta:'Up to date · 1.1 MB'}),F('Budget 2026.xlsx','sheet','186 KB','',{off:'err',meta:'Couldn\u2019t sync · Tap to retry'}),F('Recipes','folder','42 items','',{off:'ok',meta:'Up to date · 64 MB'})], + shares: [F('Projects','folder','8 items','Sep 19',{meta:'Ana Souza · Can edit',by:'Ana Souza',perm:'Can edit'}),F('Offsite photos','folder','96 items','Sep 12',{meta:'Leo Park · Can view',by:'Leo Park',perm:'Can view'}),F('Q3 Planning.pdf','pdf','2.4 MB','Yesterday',{meta:'Ana Souza · Can edit',by:'Ana Souza',perm:'Can edit'}),F('Recipes.docx','doc','44 KB','Aug 3',{meta:'Helena Rocha · Can view',by:'Helena Rocha',perm:'Can view'}),F('Lease 2026.pdf','pdf','880 KB','Aug 28',{meta:'Leo Park · Can view',by:'Leo Park',perm:'Can view'}),F('Design team','folder','212 items','Jul 30',{meta:'Design team · Can edit',by:'Design team',perm:'Can edit'})], + recent: [{label:'Today',rows:[F('Q3 Planning.pdf','pdf','2.4 MB','10:24',{meta:'Edited 10:24 · Documents',fav:1}),F('Whiteboard.jpg','image','3.1 MB','9:40',{meta:'Uploaded 9:40 · Photos'}),F('Brand brief.docx','doc','58 KB','8:12',{meta:'Opened 8:12 · Projects'})]},{label:'Yesterday',rows:[F('Budget 2026.xlsx','sheet','186 KB','',{meta:'Edited by Leo Park · Projects',shared:1}),F('Offsite recap.mp4','video','412 MB','',{meta:'Opened · Offsite photos',off:'ok'})]},{label:'This week',rows:[F('Invoices 2026.zip','zip','24 MB','',{meta:'Downloaded Mon · Documents'}),F('Lease 2026.pdf','pdf','880 KB','',{meta:'Opened Mon · Shared with you'})]}], + trash: [F('Old notes.txt','doc','4 KB','',{meta:'Deleted today · /Documents',from:'/Documents',del:'Today'}),F('Draft v1.docx','doc','62 KB','',{meta:'Deleted yesterday · /Projects',from:'/Projects',del:'Yesterday'}),F('Screenshots','folder','48 items','',{meta:'Deleted Sep 18 · /Photos',from:'/Photos',del:'Sep 18'}),F('IMG_2231.jpg','image','2.8 MB','',{meta:'Deleted Sep 15 · /Photos/Camera',from:'/Photos/Camera',del:'Sep 15'}),F('Budget 2025.xlsx','sheet','170 KB','',{meta:'Deleted Sep 2 · /Documents',from:'/Documents',del:'Sep 2'}),F('Moodboard.zip','zip','88 MB','',{meta:'Deleted Aug 29 · /Projects',from:'/Projects',del:'Aug 29'})], + activity: [{label:'Today',items:[{who:'AS',col:'#DA9AC7',name:'Ana Souza',verb:'shared',obj:'Q3 Planning.pdf',tail:'with you',time:'10:24',kind:'pdf'},{who:'LP',col:'#8DCDE2',name:'Leo Park',verb:'edited',obj:'Budget 2026.xlsx',tail:'',time:'9:02',kind:'sheet'},{who:'MR',col:'#CBA5FD',name:'You',verb:'uploaded',obj:'38 photos',tail:'to Photos/Camera',time:'8:15',kind:'image'}]},{label:'Yesterday',items:[{who:'AS',col:'#DA9AC7',name:'Ana Souza',verb:'commented on',obj:'Brand brief.docx',tail:'',time:'18:47',kind:'doc'},{who:'LP',col:'#8DCDE2',name:'Leo Park',verb:'created a link to',obj:'Offsite photos',tail:'',time:'15:10',kind:'folder'},{who:'MR',col:'#CBA5FD',name:'You',verb:'deleted',obj:'Old notes.txt',tail:'',time:'11:32',kind:'doc'}]}], + pinned: [['Files','folder','files'],['Photos','photos','photos'],['Favorites','star','favorites'],['Offline','offline','offline'],['Shares','share','shares']], + menu: [['Recent','clock','recent',''],['Activity','activity','activity',''],['Trash','trash','trash','12']], + titles: {files:'Files',photos:'Photos',activity:'Activity',trash:'Trash',favorites:'Favorites',offline:'Offline',shares:'Shares',recent:'Recent',settings:'Settings'}, + palette: ['#8DCDE2','#96B0FD','#E5D8BD','#EEEDB3','#DA9AC7','#A7D296','#E9A6A7','#85D1BD','#CBA5FD'] + }; + + function row(f, o = {}) { + const k = KIND[f.kind]; + return { ...f, meta: f.meta || `${f.size} · ${f.mod}`, tile: tile(f.kind, o.tile || 40, o.r || 12), ico: I[o.ico || 20][k[2]], + hasBadge: !!f.off, badge: BADGE[f.off] || null, shared: !!f.shared, fav: !!f.fav && !o.noFav, swiped: !!f.swiped && !o.noSwipe, + thumb: { height: o.thumbH || 104, display: 'flex', alignItems: 'center', justifyContent: 'center', background: `var(${k[0]})`, color: `var(${k[1]})` }, icoL: I[32][k[2]] }; + } + const tiles = (n, off, vids) => Array.from({ length: n }, (_, i) => ({ s: { aspectRatio: '1 / 1', background: D.palette[(i * 5 + off) % D.palette.length], position: 'relative' }, vid: vids.includes(i), dur: ['0:42', '1:08', '2:15'][i % 3] })); + const avatar = (col, s) => ({ width: s, height: s, borderRadius: 999, flex: 'none', background: col, color: '#0E0D0B', display: 'flex', alignItems: 'center', justifyContent: 'center', font: `600 ${Math.round(s * 0.36)}px/1 ${FT}` }); + + window.NK = { ic, I, TH, KIND, tile, row, tiles, avatar, D }; +})(); diff --git a/.claude/context/design-system/support.js b/.claude/context/design-system/support.js new file mode 100644 index 0000000..cb009b6 --- /dev/null +++ b/.claude/context/design-system/support.js @@ -0,0 +1,1911 @@ +// 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); + if (!window.__resources) { + 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", + onmousemove: "onMouseMove", + onmouseover: "onMouseOver", + onmouseout: "onMouseOut", + onpointerdown: "onPointerDown", + onpointerup: "onPointerUp", + onpointermove: "onPointerMove", + onpointerenter: "onPointerEnter", + onpointerleave: "onPointerLeave", + onpointercancel: "onPointerCancel", + onpointerover: "onPointerOver", + onpointerout: "onPointerOut", + ongotpointercapture: "onGotPointerCapture", + onlostpointercapture: "onLostPointerCapture", + ontouchstart: "onTouchStart", + ontouchend: "onTouchEnd", + ontouchmove: "onTouchMove", + ontouchcancel: "onTouchCancel", + ondragstart: "onDragStart", + ondragend: "onDragEnd", + ondragenter: "onDragEnter", + ondragleave: "onDragLeave", + ondragover: "onDragOver", + onanimationstart: "onAnimationStart", + onanimationend: "onAnimationEnd", + onanimationiteration: "onAnimationIteration", + ontransitionend: "onTransitionEnd" + }; + 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 encodeCamelAttrs(html) { + return html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + } + function encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = encodeCamelAttrs(html); + 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); + } + var SLIDE_ID_VALUE_RE = /^[0-9a-f]{8}$/; + var DECK_CONTROL_FLOW_RE = /^(sc-if|sc-for|sc-else|dc-import|x-import)$/; + var DECK_AUX_RE = /^(template|script|style|sc-helmet|helmet)$/; + function isDeckMountTag(el) { + if (el.localName === "deck-stage") return true; + return el.localName === "x-import" && (el.getAttribute("component-from-global-scope") || "") === "deck-stage"; + } + function walkDeckChildren(el, host) { + const pairs = [...el.childNodes].map((c) => ({ c, b: walk(c, host) })).filter((p) => p.b !== null); + const kids = pairs.map((p) => p.b); + const seen = /* @__PURE__ */ new Set(); + const wsSeen = /* @__PURE__ */ new Map(); + const keys = []; + const nextSlideId = new Array(pairs.length); + { + let upcoming = null; + for (let j = pairs.length - 1; j >= 0; j--) { + const n = pairs[j].c; + if (n.nodeType === Node.ELEMENT_NODE) { + const t = n.localName; + upcoming = !DECK_AUX_RE.test(t) && !DECK_CONTROL_FLOW_RE.test(t) ? n.getAttribute("data-om-slide-id") : null; + } + nextSlideId[j] = upcoming; + } + } + for (let j = 0; j < pairs.length; j++) { + const { c } = pairs[j]; + if (c.nodeType === Node.TEXT_NODE) { + if ((c.nodeValue ?? "").trim() === "") { + const base = nextSlideId[j] ? "omid-ws:" + nextSlideId[j] : "omid-ws:aux"; + const n = wsSeen.get(base) ?? 0; + wsSeen.set(base, n + 1); + keys.push(n === 0 ? base : base + ":" + n); + continue; + } + return { kids, keys: null }; + } + if (c.nodeType !== Node.ELEMENT_NODE) { + keys.push(j); + continue; + } + const child = c; + const tag = child.localName; + if (DECK_AUX_RE.test(tag)) { + keys.push(j); + continue; + } + if (DECK_CONTROL_FLOW_RE.test(tag)) return { kids, keys: null }; + const v = child.getAttribute("data-om-slide-id"); + if (!v || !SLIDE_ID_VALUE_RE.test(v) || seen.has(v)) { + return { kids, keys: null }; + } + seen.add(v); + keys.push("omid:" + v); + } + return { kids, keys }; + } + function renderDeckKids(kids, kidKeys, vals, ctx) { + return kids.map((b, j) => { + const k = kidKeys ? kidKeys[j] : j; + const out = b(vals, ctx, k); + return kidKeys != null && typeof out === "string" ? h(getReact().Fragment, { key: k }, out) : out; + }); + } + 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("component-from-global-scope") ? "" : 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 deckKeyed = hasContent && isDeckMountTag(el) ? walkDeckChildren(el, host) : null; + const kids = deckKeyed ? deckKeyed.kids : hasContent ? walkChildren(el, host) : []; + const kidKeys = deckKeyed?.keys ?? null; + 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 = renderDeckKids(kids, kidKeys, vals, ctx); + } + 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 deckKeyed = isDeckMountTag(el) ? walkDeckChildren(el, host) : null; + const kids = deckKeyed ? deckKeyed.kids : walkChildren(el, host); + const kidKeys = deckKeyed?.keys ?? null; + 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, ...renderDeckKids(kids, kidKeys, vals, ctx)); + }; + } + + // 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/bundled.ts + function bundledBlob(url) { + const blobs = window.__resourceBlobs; + const b = blobs ? blobs[url.split("#")[0]] : void 0; + return b instanceof Blob ? b : null; + } + + // src/cdn.ts + var REACT_URL = "https://unpkg.com/react@18.3.1/umd/react.production.min.js"; + var REACT_SRI = "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z"; + var REACT_DOM_URL = "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"; + var REACT_DOM_SRI = "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1"; + var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"; + var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"; + function cdnScriptFor(url, sri) { + const res = window.__resources; + const v = res ? res[url] : void 0; + return typeof v === "string" && v ? { src: v } : { src: url, integrity: sri }; + } + + // 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 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; + const babel = cdnScriptFor(BABEL_URL, BABEL_SRI); + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = babel.src; + if (babel.integrity) { + s.integrity = babel.integrity; + 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(() => { + const pre = bundledBlob(url); + if (pre) return pre.text(); + return 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; + 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); + if (tag === "LINK") { + const rel = (child.getAttribute("rel") || "").toLowerCase().split(/\s+/); + const href = (child.getAttribute("href") || "").trim(); + const res = window.__resources; + const pre = res && rel.includes("stylesheet") && !rel.includes("alternate") ? res[href] : void 0; + const blob = typeof pre === "string" && pre ? bundledBlob(pre) : null; + if (blob) { + const el = doc.createElement("style"); + if (child.hasAttribute("disabled")) { + el.setAttribute("media", "not all"); + } else if (child.getAttribute("media")) { + el.setAttribute("media", child.getAttribute("media")); + } + if (child.getAttribute("title")) + el.setAttribute("title", child.getAttribute("title")); + void blob.text().then((css) => { + el.textContent = css; + }); + doc.head.appendChild(el); + continue; + } + } + 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 scanUnquotedUrl(css, i) { + if (css[i] !== "u" && css[i] !== "U" || css.slice(i, i + 4).toLowerCase() !== "url(" || /[a-z0-9_-]/i.test(css[i - 1] ?? "")) { + return -1; + } + let j = i + 4; + while (j < css.length && /\s/.test(css[j])) j++; + if (css[j] === '"' || css[j] === "'") return -1; + while (j < css.length && css[j] !== ")") { + if (css[j] === "\\") j++; + j++; + } + return j < css.length ? j + 1 : css.length; + } + function stripComments(css) { + let out = ""; + let quote = ""; + for (let i = 0; i < css.length; i++) { + const c = css[i]; + if (quote) { + if (c === "\\") { + out += c + (css[i + 1] ?? ""); + i++; + continue; + } + if (c === quote) quote = ""; + out += c; + } else if (c === "'" || c === '"') { + quote = c; + out += c; + } else if (c === "/" && css[i + 1] === "*") { + const end = css.indexOf("*/", i + 2); + i = end === -1 ? css.length : end + 1; + out += " "; + } else { + const end = scanUnquotedUrl(css, i); + if (end === -1) out += c; + else { + out += css.slice(i, end); + i = end - 1; + } + } + } + return out; + } + function importantify(css) { + css = stripComments(css); + const decls = []; + let start = 0; + let depth = 0; + let quote = ""; + for (let i = 0; i < css.length; i++) { + const c = css[i]; + if (quote) { + if (c === "\\") i++; + else if (c === quote) quote = ""; + } else if (c === "'" || c === '"') quote = c; + else if (c === "(") depth++; + else if (c === ")") depth = Math.max(0, depth - 1); + else if (c === ";" && depth === 0) { + decls.push(css.slice(start, i)); + start = i + 1; + } else { + const end = scanUnquotedUrl(css, i); + if (end !== -1) i = end - 1; + } + } + decls.push(css.slice(start)); + return decls.map((d) => d.trim()).filter(Boolean).map((d) => /!\s*important$/i.test(d) ? d : d + " !important").join(";"); + } + 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 isPseudoElement = pseudo === "before" || pseudo === "after"; + const sel = isPseudoElement ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule( + sel + "{" + (isPseudoElement ? css : importantify(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"; + const res = window.__resources; + const pre = res ? res[url] : void 0; + const target = typeof pre === "string" && pre ? pre : url; + const blob = bundledBlob(target); + (blob ? blob.text() : fetch(target).then((res2) => { + if (!res2.ok) { + console.error( + '[dc-runtime] sibling fetch for "' + name + '" failed:', + url, + "returned", + res2.status, + "\u2014 the reference renders as an empty placeholder." + ); + return ""; + } + return res2.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: