@connextar/house 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +205 -28
  2. package/dist/changelog/changelog.d.ts +60 -0
  3. package/dist/changelog/changelog.d.ts.map +1 -0
  4. package/dist/changelog/changelog.js +61 -0
  5. package/dist/changelog/changelog.js.map +1 -0
  6. package/dist/changelog/index.d.ts +2 -0
  7. package/dist/changelog/index.d.ts.map +1 -0
  8. package/dist/changelog/index.js +2 -0
  9. package/dist/changelog/index.js.map +1 -0
  10. package/dist/editor/block-handle.d.ts +15 -0
  11. package/dist/editor/block-handle.d.ts.map +1 -0
  12. package/dist/editor/block-handle.js +106 -0
  13. package/dist/editor/block-handle.js.map +1 -0
  14. package/dist/editor/blocks.d.ts +35 -0
  15. package/dist/editor/blocks.d.ts.map +1 -0
  16. package/dist/editor/blocks.js +114 -0
  17. package/dist/editor/blocks.js.map +1 -0
  18. package/dist/editor/bubble-toolbar.d.ts +11 -0
  19. package/dist/editor/bubble-toolbar.d.ts.map +1 -0
  20. package/dist/editor/bubble-toolbar.js +87 -0
  21. package/dist/editor/bubble-toolbar.js.map +1 -0
  22. package/dist/editor/content-index.d.ts +2 -0
  23. package/dist/editor/content-index.d.ts.map +1 -0
  24. package/dist/editor/content-index.js +2 -0
  25. package/dist/editor/content-index.js.map +1 -0
  26. package/dist/editor/content.d.ts +30 -0
  27. package/dist/editor/content.d.ts.map +1 -0
  28. package/dist/editor/content.js +256 -0
  29. package/dist/editor/content.js.map +1 -0
  30. package/dist/editor/editor.d.ts +52 -0
  31. package/dist/editor/editor.d.ts.map +1 -0
  32. package/dist/editor/editor.js +160 -0
  33. package/dist/editor/editor.js.map +1 -0
  34. package/dist/editor/emoji-index.d.ts +2 -0
  35. package/dist/editor/emoji-index.d.ts.map +1 -0
  36. package/dist/editor/emoji-index.js +2 -0
  37. package/dist/editor/emoji-index.js.map +1 -0
  38. package/dist/editor/emoji-picker.d.ts +16 -0
  39. package/dist/editor/emoji-picker.d.ts.map +1 -0
  40. package/dist/editor/emoji-picker.js +37 -0
  41. package/dist/editor/emoji-picker.js.map +1 -0
  42. package/dist/editor/emoji.d.ts +18 -0
  43. package/dist/editor/emoji.d.ts.map +1 -0
  44. package/dist/editor/emoji.js +418 -0
  45. package/dist/editor/emoji.js.map +1 -0
  46. package/dist/editor/index.d.ts +5 -0
  47. package/dist/editor/index.d.ts.map +1 -0
  48. package/dist/editor/index.js +6 -0
  49. package/dist/editor/index.js.map +1 -0
  50. package/dist/editor/suggestion-popup.d.ts +17 -0
  51. package/dist/editor/suggestion-popup.d.ts.map +1 -0
  52. package/dist/editor/suggestion-popup.js +57 -0
  53. package/dist/editor/suggestion-popup.js.map +1 -0
  54. package/dist/editor/suggestions.d.ts +93 -0
  55. package/dist/editor/suggestions.d.ts.map +1 -0
  56. package/dist/editor/suggestions.js +179 -0
  57. package/dist/editor/suggestions.js.map +1 -0
  58. package/dist/editor/ui.d.ts +54 -0
  59. package/dist/editor/ui.d.ts.map +1 -0
  60. package/dist/editor/ui.js +58 -0
  61. package/dist/editor/ui.js.map +1 -0
  62. package/dist/house.css +267 -0
  63. package/dist/internal/cx.d.ts +3 -0
  64. package/dist/internal/cx.d.ts.map +1 -0
  65. package/dist/internal/cx.js +5 -0
  66. package/dist/internal/cx.js.map +1 -0
  67. package/dist/internal/next-link.d.ts +23 -0
  68. package/dist/internal/next-link.d.ts.map +1 -0
  69. package/dist/internal/next-link.js +3 -0
  70. package/dist/internal/next-link.js.map +1 -0
  71. package/dist/markdown/guide.d.ts +98 -0
  72. package/dist/markdown/guide.d.ts.map +1 -0
  73. package/dist/markdown/guide.js +328 -0
  74. package/dist/markdown/guide.js.map +1 -0
  75. package/dist/markdown/index.d.ts +4 -0
  76. package/dist/markdown/index.d.ts.map +1 -0
  77. package/dist/markdown/index.js +4 -0
  78. package/dist/markdown/index.js.map +1 -0
  79. package/dist/markdown/table-view.d.ts +28 -0
  80. package/dist/markdown/table-view.d.ts.map +1 -0
  81. package/dist/markdown/table-view.js +25 -0
  82. package/dist/markdown/table-view.js.map +1 -0
  83. package/dist/markdown/table.d.ts +35 -0
  84. package/dist/markdown/table.d.ts.map +1 -0
  85. package/dist/markdown/table.js +79 -0
  86. package/dist/markdown/table.js.map +1 -0
  87. package/dist/release/changelog.d.ts +72 -0
  88. package/dist/release/changelog.d.ts.map +1 -0
  89. package/dist/release/changelog.js +92 -0
  90. package/dist/release/changelog.js.map +1 -0
  91. package/dist/release/index.d.ts +3 -0
  92. package/dist/release/index.d.ts.map +1 -0
  93. package/dist/release/index.js +3 -0
  94. package/dist/release/index.js.map +1 -0
  95. package/dist/release/version.d.ts +33 -0
  96. package/dist/release/version.d.ts.map +1 -0
  97. package/dist/release/version.js +57 -0
  98. package/dist/release/version.js.map +1 -0
  99. package/dist/trail/index.d.ts +3 -0
  100. package/dist/trail/index.d.ts.map +1 -0
  101. package/dist/trail/index.js +3 -0
  102. package/dist/trail/index.js.map +1 -0
  103. package/dist/trail/rules.d.ts +44 -0
  104. package/dist/trail/rules.d.ts.map +1 -0
  105. package/dist/trail/rules.js +74 -0
  106. package/dist/trail/rules.js.map +1 -0
  107. package/dist/trail/trail.d.ts +49 -0
  108. package/dist/trail/trail.d.ts.map +1 -0
  109. package/dist/trail/trail.js +188 -0
  110. package/dist/trail/trail.js.map +1 -0
  111. package/docs/ReleaseProcess.md +315 -0
  112. package/package.json +75 -11
package/README.md CHANGED
@@ -12,16 +12,16 @@ small, and the reason it is small is worth reading before adding to it.
12
12
  The portfolio was measured before anything was extracted. Divergence, against
13
13
  Quickie, of files that exist in six or more apps:
14
14
 
15
- | File | copies | divergence |
16
- | --- | --- | --- |
17
- | `error-shell.tsx` | 6/6 | **0%** |
18
- | `check-motion.mjs` | 3/6 | 11% |
19
- | `e2e-local.sh` | 6/6 | 30% |
20
- | `button.tsx` | 6/6 | 42% |
21
- | `dialog.tsx` | 5/6 | 62% |
22
- | `card.tsx` | 5/6 | 106% |
23
- | `lib/api.ts` | 6/6 | 152% |
24
- | `auth.ts` | 4/6 | 42–103% |
15
+ | File | copies | divergence |
16
+ | ------------------ | ------ | ---------- |
17
+ | `error-shell.tsx` | 6/6 | **0%** |
18
+ | `check-motion.mjs` | 3/6 | 11% |
19
+ | `e2e-local.sh` | 6/6 | 30% |
20
+ | `button.tsx` | 6/6 | 42% |
21
+ | `dialog.tsx` | 5/6 | 62% |
22
+ | `card.tsx` | 5/6 | 106% |
23
+ | `lib/api.ts` | 6/6 | 152% |
24
+ | `auth.ts` | 4/6 | 42–103% |
25
25
 
26
26
  Everything below about 15% is an extraction. Everything above it is a rewrite
27
27
  wearing an extraction's clothes, and pulling it in here would mean thirteen apps
@@ -35,6 +35,10 @@ That is why there is no `Button` here, and why the error shell takes its actions
35
35
  as props instead of rendering its own. Bringing a second design system into an
36
36
  app that already has one is a cost with no matching benefit.
37
37
 
38
+ The 0.2.0 modules are the second half of that rule. AltEd built a versioned
39
+ changelog, a navigation trail, a block editor and guide tables; every other app
40
+ was about to write each of them for the second time.
41
+
38
42
  ## Modules
39
43
 
40
44
  ### `@connextar/house/errors`
@@ -79,7 +83,7 @@ decorative: a stolen mailbox is still enough.
79
83
 
80
84
  This module decides which routes to refuse, and why. It is here rather than in
81
85
  each app because it has to be the same everywhere and because getting it wrong is
82
- silent — two-factor keeps *looking* enabled while doing nothing.
86
+ silent — two-factor keeps _looking_ enabled while doing nothing.
83
87
 
84
88
  The lookup is injected, because only the app knows its own user table:
85
89
 
@@ -100,7 +104,7 @@ TOTP. A link in an inbox is.
100
104
  ### `@connextar/house/account`
101
105
 
102
106
  The contract for "your data" and "delete my account" — types, the export notice,
103
- and the sole-owner refusal message. The *implementation* stays per-app, because
107
+ and the sole-owner refusal message. The _implementation_ stays per-app, because
104
108
  an export is a walk over that app's Prisma schema. What must not be per-app is
105
109
  the shape of the answer, since that is what a person receives and a regulator
106
110
  reads.
@@ -119,9 +123,166 @@ Two decisions are encoded here rather than left to be rediscovered:
119
123
  App-specific sections go in `extras`, so one app's furniture does not become
120
124
  everyone's obligation.
121
125
 
126
+ ### `@connextar/house/release`
127
+
128
+ Version arithmetic and the release-history guard behind every app's changelog.
129
+ `releaseHistoryProblems(changelog, { pkg, lock })` returns, as sentences, every
130
+ way the changelog, `package.json` and the lockfile disagree — a version that
131
+ skips a step, dates out of order, a release with no title. An app's
132
+ `lib/release.test.ts` asserts it is empty. `nextVersion(current, kind)` applies
133
+ the 0.x rule (a MAJOR-class change bumps the minor before 1.0.0).
134
+
135
+ The process itself — how to classify a change, how to write the notes, the
136
+ runbook — is [`docs/ReleaseProcess.md`](docs/ReleaseProcess.md), shipped in the
137
+ package. Each app's own `docs/operations/ReleaseProcess.md` links to it and adds
138
+ only what is its own: its public API, its trigger documents, its history note.
139
+
140
+ ### `@connextar/house/changelog`
141
+
142
+ `<Changelog releases={CHANGELOG} />` — the body of a `/changelog` page. Each
143
+ release shows its version, whether it was a major, minor or patch release, its
144
+ date and a one-line summary; changes are grouped under headings, each with a bold
145
+ headline and a tag. A list of releases sits beside the content on wide screens
146
+ and becomes a scrolling strip on a phone. The app keeps its hero and calls to
147
+ action around it, and maps the four change types onto its palette with
148
+ `tagClassNames`.
149
+
150
+ ### `@connextar/house/markdown`
151
+
152
+ The help-centre guide renderer, and the GitHub-flavoured table parser under it.
153
+ Five apps carried a copy that had drifted: some rendered tables as a paragraph of
154
+ pipes, some broke a wrapped list line into a stray paragraph, some passed
155
+ `javascript:` links through, and three let an `####` heading shift the numbering
156
+ of the `##` anchors after it. `GuideMarkdown` is the union of the fixes;
157
+ `extractHeadings` gives exactly the ids it renders.
158
+
159
+ An app's `lib/markdown.tsx` becomes a thin binding that keeps its own export names,
160
+ so no page changes:
161
+
162
+ ```tsx
163
+ import { GuideMarkdown } from "@connextar/house/markdown";
164
+
165
+ import { constants } from "@lib/constants";
166
+
167
+ export {
168
+ extractHeadings as extractGuideHeadings,
169
+ extractHowToSteps,
170
+ parseGuideFrontmatter,
171
+ stripInline,
172
+ } from "@connextar/house/markdown";
173
+
174
+ export function Markdown({ content }: { content: string }) {
175
+ return <GuideMarkdown content={content} guidesPath={constants.routes.guides} />;
176
+ }
177
+ ```
178
+
179
+ `classNames` replaces the look of any element for an app without theme tokens.
180
+
181
+ ### `@connextar/house/trail`
182
+
183
+ Where you came from, rather than where the URL says you are. A breadcrumb built
184
+ from the path can only show the hierarchy the routes happen to have; a trail
185
+ shows the chain the person actually walked — a learner, then their plan, then a
186
+ lesson — and keeps it when they follow a reference to somewhere else.
187
+
188
+ Pages declare their chain (`useTrail(chain)` or `<TrailHere chain={…} />`); the
189
+ trail lives per tab in session storage under a `scope`; a link marked
190
+ `data-trail-carry` (every internal link in `RichTextContent`) carries it across.
191
+ `<TrailRail />` draws it as pills down the left on wide screens, `<TrailBar />` as
192
+ a pinned bar with Back on phones.
193
+
194
+ ```tsx
195
+ // components/navigation/dashboard-shell.tsx
196
+ "use client";
197
+ import { ScrollToHash, TrailBar, TrailProvider, TrailRail } from "@connextar/house/trail";
198
+
199
+ // `scope` keeps one signed-in person's trail from surfacing for the next on a shared browser.
200
+ export function DashboardShell({ userId, children }: { userId: string; children: React.ReactNode }) {
201
+ return (
202
+ <TrailProvider scope={userId}>
203
+ <ScrollToHash />
204
+ <div className="flex">
205
+ <TrailRail top="4rem" />
206
+ <main className="min-w-0 flex-1">
207
+ <TrailBar top="4rem" />
208
+ {children}
209
+ </main>
210
+ </div>
211
+ </TrailProvider>
212
+ );
213
+ }
214
+ ```
215
+
216
+ ### `@connextar/house/editor`, `/editor/content`, `/editor/emoji`
217
+
218
+ `RichTextEditor` — Notion-style writing for any prose field. Formatting appears
219
+ as a bubble over a selection (with Turn into); new blocks come from typing `/` or
220
+ the `+` beside a block; every block has a grip to drag or open its menu (turn
221
+ into, duplicate, move up, move down, delete); `:` completes an emoji shortcode.
222
+ `@` references and `#` tags work when the app passes `sources`, because only the
223
+ app knows what its people, records and tags are. **Storage is plain Markdown with
224
+ no new node types** — a reference is `[@Ann Lee](/people/1)` — so every existing
225
+ text column keeps working and a server can read mentions with a regex.
226
+
227
+ `RichTextContent` (from `/editor/content`) is the read-only side: Markdown to
228
+ React with no HTML, an allow-list of link schemes, and bounded patterns, safe for
229
+ text a stranger wrote. `EmojiPicker` and the curated set of ~360 emoji are in
230
+ `/editor` and `/editor/emoji`.
231
+
232
+ The editor draws its own menus and popovers on Radix with the house token
233
+ classes. They are internal and not exported: the apps' primitive sets differ too
234
+ much for the editor to ask for theirs, and this is not a second design system.
235
+
236
+ If an app also depends on `@tiptap/*` directly, keep it in the same range as this
237
+ package so npm installs one copy. Two copies of `@tiptap/pm` break the editor.
238
+
239
+ ### Styles — `@connextar/house/house.css`
240
+
241
+ What the components need that utility classes cannot say: rich text, the editor's
242
+ block gutter and suggestion highlight, the trail's tinted pills.
243
+
244
+ ## Tailwind setup (required for every component)
245
+
246
+ Tailwind does not scan `node_modules`. Every utility class that appears only
247
+ inside this package is silently missing from an app's build unless the app
248
+ points Tailwind at it. This was measured, not assumed: a probe compile of an
249
+ app's stylesheet emitted none of the package's classes until the line below was
250
+ added.
251
+
252
+ **Tailwind v4** — in the global stylesheet:
253
+
254
+ ```css
255
+ @import "tailwindcss";
256
+ @import "@connextar/house/house.css";
257
+ @source "../node_modules/@connextar/house/dist";
258
+ ```
259
+
260
+ (The `@source` path is relative to the stylesheet.)
261
+
262
+ **Tailwind v3** — add `"./node_modules/@connextar/house/dist/**/*.js"` to
263
+ `content` in `tailwind.config`, and import `@connextar/house/house.css` at the top
264
+ of the global stylesheet, before the `@tailwind` directives.
265
+
266
+ `house.css` reads the app's theme tokens (`--foreground`, `--muted-foreground`,
267
+ `--primary`, `--muted`, `--border`, `--card`, `--accent`) through a `--house-*`
268
+ variable at each use. An app that stores tokens as HSL triplets
269
+ (`--primary: 222 47% 11%`) maps them once:
270
+
271
+ ```css
272
+ :root {
273
+ --house-foreground: hsl(var(--foreground));
274
+ --house-muted-foreground: hsl(var(--muted-foreground));
275
+ --house-primary: hsl(var(--primary));
276
+ --house-muted: hsl(var(--muted));
277
+ --house-border: hsl(var(--border));
278
+ --house-card: hsl(var(--card));
279
+ --house-accent: hsl(var(--accent));
280
+ }
281
+ ```
282
+
122
283
  ## Publishing
123
284
 
124
- The scope is public and the contents are a design system and two policy modules —
285
+ The scope is public and the contents are shared components and policy modules —
125
286
  nothing here is a secret. Public publishing is what keeps thirteen repositories
126
287
  across two GitHub owners able to install it with no token in any Vercel project,
127
288
  which is the failure mode that would otherwise take every deploy down at once.
@@ -136,24 +297,40 @@ npm publish # publishConfig.access is already "public"
136
297
 
137
298
  ## Adopting it in an app
138
299
 
139
- Once published:
140
-
141
300
  ```bash
142
301
  npm install @connextar/house
143
302
  ```
144
303
 
145
- Then replace the local copies. For Quickie the exact change is saved as a patch,
146
- generated from the working migration and verified against a real packed tarball:
147
-
148
- ```bash
149
- git apply ../../Shared/cx-house/migrations/quickie-adopt-house.patch
150
- npm run check && npm test && npm run build
151
- ```
152
-
153
- For the other apps the shape is the same: `global-error.tsx` becomes a call to
154
- `GlobalErrorShell`, `components/errors/error-shell.tsx` re-exports `ErrorShell`
155
- and keeps only its own `ErrorAction`, and any two-factor policy defers to
156
- `@connextar/house/auth`.
304
+ Then, per module the app takes up:
305
+
306
+ 1. **Tailwind first.** Add the `@source` line (or the v3 `content` glob) from
307
+ [Tailwind setup](#tailwind-setup-required-for-every-component). Without it the
308
+ components render unstyled and nothing errors.
309
+ 2. **Replace the local copy with a binding, not a rewrite of its callers.** The
310
+ app's `lib/markdown.tsx`, `components/editor/rich-text-editor.tsx` or
311
+ `lib/changelog.ts` stays where its importers expect it and shrinks to the
312
+ app's own part: its routes, its type scale, its `@` and `#` lookups, its data.
313
+ A binding that grows past a screen is the package being re-implemented.
314
+ 3. **Move the tests with the code.** A local test of behaviour the package now
315
+ owns is deleted; a test of the app's binding stays.
316
+ 4. **Changelog and releases** follow [`docs/ReleaseProcess.md`](docs/ReleaseProcess.md)
317
+ §7: the old entries become numbered releases once, `lib/release.test.ts` guards
318
+ them from then on, and the app's own `docs/operations/ReleaseProcess.md` says
319
+ only what is its own.
320
+ 5. **Gates, then the pages.** `npm run check`, `npm test`, `npm run build`, and
321
+ grep the built CSS for a class that only the package uses — the one check that
322
+ proves step 1.
323
+
324
+ Traps found adopting it the first time:
325
+
326
+ - A **server component** receives a client module as a reference that throws on
327
+ any property access. Never dot into an import from a client module in shared
328
+ code; the package's own `next/link` helper is a type-only cast for this reason.
329
+ - Next writes route types under `.next/dev/types` during `next dev`. After routes
330
+ are deleted, a stale copy fails `next build`'s type check with "Cannot find
331
+ module"; delete `.next/dev` when no dev server is running.
332
+ - If the app also depends on `@tiptap/*` directly, keep the range aligned or drop
333
+ the direct dependency — two copies of `@tiptap/pm` break the editor.
157
334
 
158
335
  **A note on local development.** Installing this with `npm install ../path` makes
159
336
  a symlink, and Next's Turbopack will not resolve a package whose real path is
@@ -163,7 +340,7 @@ behaves exactly like a registry install:
163
340
 
164
341
  ```bash
165
342
  npm pack --pack-destination /tmp # in this repo
166
- npm install /tmp/connextar-house-0.1.0.tgz # in the app
343
+ npm install /tmp/connextar-house-0.2.0.tgz # in the app
167
344
  ```
168
345
 
169
346
  ## Adding to it
@@ -0,0 +1,60 @@
1
+ import type { ReactNode } from "react";
2
+ import { type ChangeType, type ChangelogRelease } from "../release/changelog.js";
3
+ import { type ReleaseKind } from "../release/version.js";
4
+ /**
5
+ * The body of a /changelog page: an index of releases beside the releases
6
+ * themselves.
7
+ *
8
+ * Each release shows its version, whether it is a major, minor or patch
9
+ * release, and its date, then a headline and a one-sentence summary. Changes
10
+ * sit under the product-area headings they were grouped by, each with a tag
11
+ * and a bold headline. The index is a sticky rail on wide screens and a
12
+ * scrolling strip of versions on a phone.
13
+ *
14
+ * Only the body: the page's header, hero, footer and metadata are the app's,
15
+ * because that is where every product already differs. Server-safe, no hooks.
16
+ *
17
+ * Tag colours default to tokens every house app has (`primary`, `accent`,
18
+ * `muted`, `border`). An app with `success` and `warning` tokens can pass
19
+ * `tagClassNames` for the fuller palette.
20
+ */
21
+ export declare const DEFAULT_TAG_CLASS_NAMES: Record<ChangeType, string>;
22
+ export declare const TAG_LABELS: Record<ChangeType, string>;
23
+ export declare const KIND_LABELS: Record<ReleaseKind, string>;
24
+ export interface ChangelogProps {
25
+ releases: readonly ChangelogRelease[];
26
+ /** Tag classes per change type; merged over the defaults. */
27
+ tagClassNames?: Partial<Record<ChangeType, string>>;
28
+ /** The release headline. Defaults to a plain 2xl semibold heading. */
29
+ releaseTitleClassName?: string;
30
+ /** The small uppercase label above the index. */
31
+ eyebrowClassName?: string;
32
+ /** The one-sentence summary under a release headline. */
33
+ summaryClassName?: string;
34
+ /** The "Latest" pill's colours. */
35
+ latestClassName?: string;
36
+ /** A section heading ("Notes", "Fixes"). */
37
+ sectionHeadingClassName?: string;
38
+ /** The "3 changes" count beside a section heading. */
39
+ sectionCountClassName?: string;
40
+ /**
41
+ * How a change's text is drawn. Defaults to rendering `**bold**` as strong —
42
+ * release notes name UI labels that way — and everything else as text.
43
+ */
44
+ renderText?: (text: string) => ReactNode;
45
+ /** Locale for release dates. */
46
+ locale?: string;
47
+ /** How far below the top a sticky index sits, and where an anchor lands. */
48
+ stickyTop?: string;
49
+ className?: string;
50
+ }
51
+ /** `**label**` → <strong>label</strong>; nothing else is interpreted. */
52
+ export declare function renderReleaseText(text: string): ReactNode;
53
+ export declare function Changelog({ releases, tagClassNames, releaseTitleClassName, eyebrowClassName, summaryClassName, latestClassName, sectionHeadingClassName, sectionCountClassName, renderText, locale, stickyTop, className, }: ChangelogProps): import("react").JSX.Element;
54
+ /** Figures for a page hero: the latest version, how many releases, how many changes. */
55
+ export declare function changelogStats(releases: readonly ChangelogRelease[]): {
56
+ latest: string | null;
57
+ releases: number;
58
+ changes: number;
59
+ };
60
+ //# sourceMappingURL=changelog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changelog.d.ts","sourceRoot":"","sources":["../../src/changelog/changelog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEtE;;;;;;;;;;;;;;;;GAgBG;AAEH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAK9D,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAKjD,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAInD,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACtC,6DAA6D;IAC7D,aAAa,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,sEAAsE;IACtE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iDAAiD;IACjD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mCAAmC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4CAA4C;IAC5C,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,sDAAsD;IACtD,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,CAAC;IACzC,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAWzD;AAED,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,aAAa,EACb,qBAAwF,EACxF,gBAA4F,EAC5F,gBAAgF,EAChF,eAAqE,EACrE,uBAAyF,EACzF,qBAAmG,EACnG,UAA8B,EAC9B,MAAgB,EAChB,SAAkB,EAClB,SAAS,GACV,EAAE,cAAc,+BA0GhB;AAED,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,GAAG;IACrE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAMA"}
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../internal/cx.js";
3
+ import { countChanges, formatReleaseDate, releaseAnchor, } from "../release/changelog.js";
4
+ import { releaseKind } from "../release/version.js";
5
+ /**
6
+ * The body of a /changelog page: an index of releases beside the releases
7
+ * themselves.
8
+ *
9
+ * Each release shows its version, whether it is a major, minor or patch
10
+ * release, and its date, then a headline and a one-sentence summary. Changes
11
+ * sit under the product-area headings they were grouped by, each with a tag
12
+ * and a bold headline. The index is a sticky rail on wide screens and a
13
+ * scrolling strip of versions on a phone.
14
+ *
15
+ * Only the body: the page's header, hero, footer and metadata are the app's,
16
+ * because that is where every product already differs. Server-safe, no hooks.
17
+ *
18
+ * Tag colours default to tokens every house app has (`primary`, `accent`,
19
+ * `muted`, `border`). An app with `success` and `warning` tokens can pass
20
+ * `tagClassNames` for the fuller palette.
21
+ */
22
+ export const DEFAULT_TAG_CLASS_NAMES = {
23
+ new: "border-primary/40 bg-primary/10 text-primary",
24
+ improved: "border-border bg-accent text-accent-foreground",
25
+ changed: "border-foreground/30 bg-background text-foreground",
26
+ fixed: "border-border bg-muted text-muted-foreground",
27
+ };
28
+ export const TAG_LABELS = {
29
+ new: "New",
30
+ improved: "Improved",
31
+ changed: "Changed",
32
+ fixed: "Fixed",
33
+ };
34
+ export const KIND_LABELS = {
35
+ major: "Major release",
36
+ minor: "Minor release",
37
+ patch: "Patch release",
38
+ };
39
+ /** `**label**` → <strong>label</strong>; nothing else is interpreted. */
40
+ export function renderReleaseText(text) {
41
+ const parts = text.split(/(\*\*[^*\n]+\*\*)/g);
42
+ return parts.map((part, i) => part.startsWith("**") && part.endsWith("**") && part.length > 4 ? (_jsx("strong", { className: "font-semibold text-foreground", children: part.slice(2, -2) }, i)) : (part));
43
+ }
44
+ export function Changelog({ releases, tagClassNames, releaseTitleClassName = "text-2xl font-semibold tracking-tight text-balance sm:text-3xl", eyebrowClassName = "text-xs font-semibold tracking-[0.14em] text-muted-foreground uppercase", summaryClassName = "max-w-prose text-base leading-relaxed text-muted-foreground", latestClassName = "border-primary bg-primary text-primary-foreground", sectionHeadingClassName = "text-xs font-bold tracking-[0.14em] text-foreground uppercase", sectionCountClassName = "text-[11px] font-medium tracking-normal text-muted-foreground normal-case", renderText = renderReleaseText, locale = "en-GB", stickyTop = "7rem", className, }) {
45
+ const tags = { ...DEFAULT_TAG_CLASS_NAMES, ...tagClassNames };
46
+ return (_jsxs("div", { className: cx("mx-auto grid max-w-6xl gap-10 px-4 py-12 lg:grid-cols-[14rem_minmax(0,1fr)] lg:gap-16", className), children: [_jsx("nav", { "aria-label": "Releases", className: "min-w-0", children: _jsxs("div", { className: "lg:sticky", style: { top: stickyTop }, children: [_jsx("p", { className: eyebrowClassName, children: "Releases" }), _jsx("ol", { className: "-mx-4 mt-4 flex gap-2 overflow-x-auto px-4 pb-2 lg:mx-0 lg:block lg:max-h-[calc(100vh-10rem)] lg:space-y-0.5 lg:overflow-y-auto lg:border-l lg:border-border lg:px-0 lg:pb-0", children: releases.map((release) => (_jsx("li", { className: "shrink-0", children: _jsxs("a", { href: `#${releaseAnchor(release.version)}`, className: "block rounded-full border border-border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:border-primary hover:text-foreground lg:-ml-px lg:rounded-none lg:border-0 lg:border-l-2 lg:border-transparent lg:py-1.5 lg:pr-2 lg:pl-4", children: [_jsxs("span", { className: "font-mono text-xs font-semibold text-foreground", children: ["v", release.version] }), _jsx("span", { className: "hidden truncate lg:block", children: release.title })] }) }, release.version))) })] }) }), _jsx("div", { className: "min-w-0 space-y-20", children: releases.map((release, i) => {
47
+ const anchor = releaseAnchor(release.version);
48
+ const previous = releases[i + 1];
49
+ const kind = previous ? releaseKind(previous.version, release.version) : null;
50
+ return (_jsxs("article", { id: anchor, "aria-labelledby": `${anchor}-title`, style: { scrollMarginTop: stickyTop }, children: [_jsxs("header", { className: "border-b border-border pb-7", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2", children: [_jsxs("span", { className: "rounded-full border border-primary/40 bg-primary/10 px-2.5 py-0.5 font-mono text-xs font-semibold text-primary", children: ["v", release.version] }), i === 0 ? (_jsx("span", { className: cx("rounded-full border px-2.5 py-0.5 text-[11px] font-semibold", latestClassName), children: "Latest" })) : null, kind ? _jsx("span", { className: "text-xs text-muted-foreground", children: KIND_LABELS[kind] }) : null, _jsx("span", { className: "text-xs text-muted-foreground", "aria-hidden": "true", children: "\u00B7" }), _jsx("time", { dateTime: release.date, className: "text-xs text-muted-foreground", children: formatReleaseDate(release.date, locale) })] }), _jsx("h2", { id: `${anchor}-title`, className: cx("mt-4", releaseTitleClassName), children: release.title }), _jsx("p", { className: cx("mt-3", summaryClassName), children: release.summary })] }), _jsx("div", { className: "mt-9 space-y-10", children: release.sections.map((section) => (_jsxs("section", { "aria-label": `${section.heading} — v${release.version}`, children: [_jsxs("h3", { className: cx("flex items-baseline gap-2", sectionHeadingClassName), children: [section.heading, _jsxs("span", { className: sectionCountClassName, children: [section.changes.length, " ", section.changes.length === 1 ? "change" : "changes"] })] }), _jsx("ul", { className: "mt-4 divide-y divide-border overflow-hidden rounded-2xl border border-border bg-card", children: section.changes.map((change) => (_jsxs("li", { className: "grid gap-2.5 p-5 sm:grid-cols-[5.5rem_minmax(0,1fr)] sm:gap-5", children: [_jsx("span", { className: cx("inline-flex h-fit w-fit items-center rounded-full border px-2 py-0.5 text-[11px] font-semibold", tags[change.type]), children: TAG_LABELS[change.type] }), _jsxs("div", { children: [_jsx("p", { className: "leading-snug font-semibold text-card-foreground", children: change.title }), _jsx("p", { className: "mt-1.5 text-sm leading-relaxed text-muted-foreground", children: renderText(change.text) })] })] }, change.title))) })] }, section.heading))) })] }, release.version));
51
+ }) })] }));
52
+ }
53
+ /** Figures for a page hero: the latest version, how many releases, how many changes. */
54
+ export function changelogStats(releases) {
55
+ return {
56
+ latest: releases[0]?.version ?? null,
57
+ releases: releases.length,
58
+ changes: releases.reduce((n, release) => n + countChanges(release), 0),
59
+ };
60
+ }
61
+ //# sourceMappingURL=changelog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changelog.js","sourceRoot":"","sources":["../../src/changelog/changelog.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { Changelog, changelogStats, DEFAULT_TAG_CLASS_NAMES, KIND_LABELS, renderReleaseText, TAG_LABELS, type ChangelogProps, } from "./changelog.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/changelog/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,cAAc,EACd,uBAAuB,EACvB,WAAW,EACX,iBAAiB,EACjB,UAAU,EACV,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { Changelog, changelogStats, DEFAULT_TAG_CLASS_NAMES, KIND_LABELS, renderReleaseText, TAG_LABELS, } from "./changelog.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/changelog/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,cAAc,EACd,uBAAuB,EACvB,WAAW,EACX,iBAAiB,EACjB,UAAU,GAEX,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Editor } from "@tiptap/react";
2
+ /**
3
+ * The gutter beside whichever block the pointer is over: a `+` that starts a new
4
+ * block underneath (with the `/` menu already open, as Notion does) and a grip
5
+ * that drags the block to reorder it or, clicked, opens the block menu — turn
6
+ * into, duplicate, move up, move down, delete.
7
+ *
8
+ * Everything works on the block's document position rather than the caret, so a
9
+ * block can be acted on without ever being clicked into. The drag itself is the
10
+ * extension's: it understands nested lists and shows the drop cursor.
11
+ */
12
+ export declare function BlockHandle({ editor }: {
13
+ editor: Editor;
14
+ }): import("react").JSX.Element;
15
+ //# sourceMappingURL=block-handle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-handle.d.ts","sourceRoot":"","sources":["../../src/editor/block-handle.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AA+B5C;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,+BAuJzD"}
@@ -0,0 +1,106 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { DragHandle } from "@tiptap/extension-drag-handle-react";
4
+ import { ArrowDown, ArrowUp, Check, Copy, GripVertical, Plus, Trash2 } from "lucide-react";
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ import { BLOCK_TYPES } from "./blocks.js";
7
+ import { ghostButtonClass, Menu, MenuContent, MenuItem, MenuSeparator, MenuSub, MenuSubContent, MenuSubTrigger, MenuTrigger, } from "./ui.js";
8
+ /**
9
+ * Stable on purpose: the DragHandle wrapper re-registers its plugin when this
10
+ * changes identity, and a plugin re-registration recreates every plugin view in
11
+ * the editor — see the note in bubble-toolbar.tsx.
12
+ */
13
+ const HANDLE_POSITION = { placement: "left-start" };
14
+ const handleButtonClass = `${ghostButtonClass} size-6 p-0 text-muted-foreground hover:text-foreground`;
15
+ /**
16
+ * The gutter beside whichever block the pointer is over: a `+` that starts a new
17
+ * block underneath (with the `/` menu already open, as Notion does) and a grip
18
+ * that drags the block to reorder it or, clicked, opens the block menu — turn
19
+ * into, duplicate, move up, move down, delete.
20
+ *
21
+ * Everything works on the block's document position rather than the caret, so a
22
+ * block can be acted on without ever being clicked into. The drag itself is the
23
+ * extension's: it understands nested lists and shows the drop cursor.
24
+ */
25
+ export function BlockHandle({ editor }) {
26
+ const [hovered, setHovered] = useState(null);
27
+ const [menuOpen, setMenuOpen] = useState(false);
28
+ const menuOpenRef = useRef(false);
29
+ useEffect(() => {
30
+ menuOpenRef.current = menuOpen;
31
+ }, [menuOpen]);
32
+ // Leave the last block in place while its menu is open, so the menu still
33
+ // knows what it belongs to after the pointer wanders. Stable identity for the
34
+ // same reason as HANDLE_POSITION.
35
+ const onNodeChange = useCallback(({ node, pos }) => {
36
+ if (menuOpenRef.current)
37
+ return;
38
+ setHovered(node ? { node, pos } : null);
39
+ }, []);
40
+ const range = (block) => ({ from: block.pos, to: block.pos + block.node.nodeSize });
41
+ /** Turn the whole block into another type, without moving the caret by hand. */
42
+ const turnInto = (block, typeId) => {
43
+ const type = BLOCK_TYPES.find((t) => t.id === typeId);
44
+ if (!type)
45
+ return;
46
+ const inside = { from: block.pos + 1, to: Math.max(block.pos + 1, block.pos + block.node.nodeSize - 1) };
47
+ type.apply(editor.chain().focus().setTextSelection(inside)).run();
48
+ };
49
+ const duplicate = (block) => {
50
+ // The node is immutable, so it can be inserted twice.
51
+ editor
52
+ .chain()
53
+ .focus()
54
+ .insertContentAt(block.pos + block.node.nodeSize, block.node)
55
+ .run();
56
+ };
57
+ const remove = (block) => {
58
+ editor.chain().focus().deleteRange(range(block)).run();
59
+ };
60
+ /** Swap the block with its previous (-1) or next (+1) sibling. */
61
+ const move = (block, direction) => {
62
+ const $pos = editor.state.doc.resolve(block.pos);
63
+ const parent = $pos.parent;
64
+ const siblingIndex = $pos.index() + direction;
65
+ if (siblingIndex < 0 || siblingIndex >= parent.childCount)
66
+ return;
67
+ const sibling = parent.child(siblingIndex);
68
+ const { from, to } = range(block);
69
+ editor
70
+ .chain()
71
+ .focus()
72
+ .command(({ tr }) => {
73
+ tr.delete(from, to);
74
+ const insertAt = direction === -1 ? from - sibling.nodeSize : from + sibling.nodeSize;
75
+ tr.insert(insertAt, block.node);
76
+ return true;
77
+ })
78
+ .run();
79
+ };
80
+ /**
81
+ * The `+`: a fresh paragraph under this block with the `/` menu open — or, if
82
+ * this block is already an empty paragraph, just the menu, right here.
83
+ */
84
+ const insertBelow = (block) => {
85
+ const isEmptyParagraph = block.node.type.name === "paragraph" && block.node.content.size === 0;
86
+ if (isEmptyParagraph) {
87
+ editor
88
+ .chain()
89
+ .focus()
90
+ .setTextSelection(block.pos + 1)
91
+ .insertContent("/")
92
+ .run();
93
+ return;
94
+ }
95
+ const after = block.pos + block.node.nodeSize;
96
+ editor
97
+ .chain()
98
+ .focus()
99
+ .insertContentAt(after, { type: "paragraph" })
100
+ .setTextSelection(after + 1)
101
+ .insertContent("/")
102
+ .run();
103
+ };
104
+ return (_jsxs(DragHandle, { editor: editor, nested: true, className: "house-block-handle", computePositionConfig: HANDLE_POSITION, onNodeChange: onNodeChange, children: [_jsx("button", { type: "button", className: handleButtonClass, "aria-label": "Add a block below", title: "Add a block below", onClick: () => hovered && insertBelow(hovered), children: _jsx(Plus, { className: "size-4", "aria-hidden": true }) }), _jsxs(Menu, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(MenuTrigger, { asChild: true, children: _jsx("button", { type: "button", className: `${handleButtonClass} cursor-grab active:cursor-grabbing`, "aria-label": "Block options \u2014 drag to move", title: "Drag to move, click for options", children: _jsx(GripVertical, { className: "size-4", "aria-hidden": true }) }) }), _jsxs(MenuContent, { className: "w-48", children: [_jsxs(MenuSub, { children: [_jsx(MenuSubTrigger, { children: "Turn into" }), _jsx(MenuSubContent, { className: "w-48", children: BLOCK_TYPES.map((type) => (_jsxs(MenuItem, { onSelect: () => hovered && turnInto(hovered, type.id), children: [_jsx(type.icon, { className: "size-4", "aria-hidden": true }), _jsx("span", { className: "flex-1", children: type.label }), hovered && type.matchesNode(hovered.node) ? (_jsx(Check, { className: "size-4 text-primary", "aria-hidden": true })) : null] }, type.id))) })] }), _jsxs(MenuItem, { onSelect: () => hovered && duplicate(hovered), children: [_jsx(Copy, { className: "size-4", "aria-hidden": true }), "Duplicate"] }), _jsxs(MenuItem, { onSelect: () => hovered && move(hovered, -1), children: [_jsx(ArrowUp, { className: "size-4", "aria-hidden": true }), "Move up"] }), _jsxs(MenuItem, { onSelect: () => hovered && move(hovered, 1), children: [_jsx(ArrowDown, { className: "size-4", "aria-hidden": true }), "Move down"] }), _jsx(MenuSeparator, {}), _jsxs(MenuItem, { className: "text-destructive focus:text-destructive", onSelect: () => hovered && remove(hovered), children: [_jsx(Trash2, { className: "size-4", "aria-hidden": true }), "Delete"] })] })] })] }));
105
+ }
106
+ //# sourceMappingURL=block-handle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-handle.js","sourceRoot":"","sources":["../../src/editor/block-handle.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { ChainedCommands, Editor } from "@tiptap/core";
2
+ import type { Node as PMNode } from "@tiptap/pm/model";
3
+ import { type LucideIcon } from "lucide-react";
4
+ /**
5
+ * The block vocabulary of the editor — one list, read by three surfaces: the
6
+ * `/` menu, the bubble menu's "Turn into", and each block's grip menu.
7
+ *
8
+ * These are exactly the blocks Markdown can say, which is the whole reason there
9
+ * are no callouts, toggles or columns: the stored format is Markdown text, and a
10
+ * block the read-only renderer (and every printed or emailed copy) could not show
11
+ * would silently degrade on the surfaces people hand to someone else.
12
+ */
13
+ export interface BlockType {
14
+ id: "paragraph" | "heading1" | "heading2" | "heading3" | "bulletList" | "orderedList" | "taskList" | "blockquote" | "codeBlock";
15
+ label: string;
16
+ description: string;
17
+ icon: LucideIcon;
18
+ /** Extra words the `/` menu matches on. */
19
+ keywords: string[];
20
+ /** Markdown shortcut shown as a hint, e.g. "# ". */
21
+ shortcut: string;
22
+ /**
23
+ * Convert whatever the selection is in. `clearNodes()` first lifts the
24
+ * selection out of lists and quotes, so "turn a quote into a heading" and
25
+ * "turn a list into paragraphs" both work in one step.
26
+ */
27
+ apply: (chain: ChainedCommands) => ChainedCommands;
28
+ isActive: (editor: Editor) => boolean;
29
+ /** Whether a given block node (from the grip, not the caret) is this type. */
30
+ matchesNode: (node: PMNode) => boolean;
31
+ }
32
+ export declare const BLOCK_TYPES: readonly BlockType[];
33
+ /** The block type the selection currently sits in, for "Turn into" state. */
34
+ export declare function activeBlockType(editor: Editor): BlockType;
35
+ //# sourceMappingURL=blocks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../../src/editor/blocks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,KAAK,EAAE,IAAI,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAUL,KAAK,UAAU,EAChB,MAAM,cAAc,CAAC;AAEtB;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,EAAE,EACE,WAAW,GACX,UAAU,GACV,UAAU,GACV,UAAU,GACV,YAAY,GACZ,aAAa,GACb,UAAU,GACV,YAAY,GACZ,WAAW,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,UAAU,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,eAAe,CAAC;IACnD,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IACtC,8EAA8E;IAC9E,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACxC;AAsBD,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EA0F3C,CAAC;AAEF,6EAA6E;AAC7E,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAEzD"}