storyloco 2.9.0 → 2.11.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.
- package/README.md +125 -0
- package/package.json +54 -10
- package/packages/churchsuite_categories/types.ts +9 -0
- package/packages/shared/dist/components/ui/button/button.svelte.d.ts +33 -33
- package/packages/shared/dist/components/ui/select/select-group.svelte +1 -1
- package/packages/shared/dist/components/ui/textarea/textarea.svelte +1 -2
- package/packages/vite/dist/editor/anchor.d.ts +23 -0
- package/packages/vite/dist/editor/anchor.js +92 -0
- package/packages/vite/dist/editor/block_bar.svelte +624 -0
- package/packages/vite/dist/editor/block_bar.svelte.d.ts +36 -0
- package/packages/vite/dist/editor/client.d.ts +202 -0
- package/packages/vite/dist/editor/client.js +473 -0
- package/packages/vite/dist/editor/comments.svelte +1386 -0
- package/packages/vite/dist/editor/comments.svelte.d.ts +27 -0
- package/packages/vite/dist/editor/compare.svelte +716 -0
- package/packages/vite/dist/editor/compare.svelte.d.ts +11 -0
- package/packages/vite/dist/editor/content.d.ts +85 -0
- package/packages/vite/dist/editor/content.js +360 -0
- package/packages/vite/dist/editor/edit_bar.svelte +1712 -0
- package/packages/vite/dist/editor/edit_bar.svelte.d.ts +29 -0
- package/packages/vite/dist/editor/face.svelte +69 -0
- package/packages/vite/dist/editor/face.svelte.d.ts +15 -0
- package/packages/vite/dist/editor/image_picker.svelte +645 -0
- package/packages/vite/dist/editor/image_picker.svelte.d.ts +36 -0
- package/packages/vite/dist/editor/index.d.ts +2 -0
- package/packages/vite/dist/editor/index.js +2 -0
- package/packages/vite/dist/editor/inspector.svelte +2650 -0
- package/packages/vite/dist/editor/inspector.svelte.d.ts +85 -0
- package/packages/vite/dist/editor/live.d.ts +105 -0
- package/packages/vite/dist/editor/live.js +419 -0
- package/packages/vite/dist/editor/overlay.d.ts +81 -0
- package/packages/vite/dist/editor/overlay.js +1369 -0
- package/packages/vite/dist/editor/presence.d.ts +14 -0
- package/packages/vite/dist/editor/presence.js +399 -0
- package/packages/vite/dist/editor/richtext.d.ts +93 -0
- package/packages/vite/dist/editor/richtext.js +1147 -0
- package/packages/vite/dist/editor/server.d.ts +139 -0
- package/packages/vite/dist/editor/server.js +1320 -0
- package/packages/vite/dist/editor/time_machine.svelte +1235 -0
- package/packages/vite/dist/editor/time_machine.svelte.d.ts +12 -0
- package/packages/vite/dist/editor/together.d.ts +34 -0
- package/packages/vite/dist/editor/together.js +106 -0
- package/packages/vite/dist/editor/tools.svelte +2323 -0
- package/packages/vite/dist/editor/tools.svelte.d.ts +23 -0
- package/packages/vite/dist/hooks.d.ts +19 -3
- package/packages/vite/dist/hooks.js +24 -4
- package/packages/vite/dist/kit.d.ts +22 -0
- package/packages/vite/dist/kit.js +1 -0
- package/packages/vite/dist/middleware.d.ts +37 -0
- package/packages/vite/dist/middleware.js +43 -0
- package/packages/vite/dist/redirects/handle.d.ts +1 -1
- package/packages/vite/dist/server.d.ts +4 -4
- package/packages/vite/dist/storyblok.svelte.d.ts +10 -4
- package/packages/vite/dist/storyblok.svelte.js +45 -15
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, a
|
|
|
17
17
|
- **⏰ Time** - Simple time input field with step control
|
|
18
18
|
- **💷 Currency** - Amount input with a currency selector; configure allowed codes via the `currencies` and `default_currency` options
|
|
19
19
|
- **📅 Dates** - List of dates, each with an optional time, end date and end time, plus a timezone picker; configure via the `min`, `max` and `auto_sort` options
|
|
20
|
+
- **⛪ ChurchSuite Categories** - Multi-select of ChurchSuite calendar categories, read from your site's endpoint via the `endpoint` option
|
|
20
21
|
|
|
21
22
|
### Vite Plugins
|
|
22
23
|
|
|
@@ -114,6 +115,19 @@ API methods await it, so you never call it on the server. `components` takes the
|
|
|
114
115
|
`import.meta.glob`: Vite has to see the literal pattern to rewrite it, so the glob has to stay
|
|
115
116
|
at your call site rather than move into the client.
|
|
116
117
|
|
|
118
|
+
`api` goes to the SDK as its `apiOptions`. To keep the token off the browser, read through a
|
|
119
|
+
proxy of your own that adds it, and give the client a stand-in:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
export const client = new StoryblokClient({
|
|
123
|
+
token: 'proxy',
|
|
124
|
+
// the SDK wants a whole URL
|
|
125
|
+
api: { endpoint: `${browser ? location.origin : 'http://localhost'}/api/storyblok` }
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The editor then takes the real token from `STORYBLOK_ACCESS_TOKEN`.
|
|
130
|
+
|
|
117
131
|
`client.handle_error` is on the instance, so `.catch(client.handle_error)` works without
|
|
118
132
|
wrapping anything.
|
|
119
133
|
|
|
@@ -270,6 +284,110 @@ await client.get('cdn/datasource_entries', { version })
|
|
|
270
284
|
|
|
271
285
|
Errors go through `client.handle_error`, so a missing story is a 404 and anything else a 500, shaped by the client's `error` option.
|
|
272
286
|
|
|
287
|
+
### Edit on the Site
|
|
288
|
+
|
|
289
|
+
Anyone with a role in the space can edit the real site in place, signed in with their own
|
|
290
|
+
Storyblok account, so their permissions apply and every change is credited to them. Changes
|
|
291
|
+
save as drafts; Publish in the corner badge lists what will go live, block by block, first.
|
|
292
|
+
|
|
293
|
+
It needs the **Site Editor** app installed in the space (Storyblok › Apps), with the site's
|
|
294
|
+
origin allowed on the app's page. `localhost` always is, and a pattern like
|
|
295
|
+
`https://*-team.vercel.app` covers every preview. Then three lines:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// hooks.server.ts
|
|
299
|
+
import { editor } from 'storyloco/editor/server'
|
|
300
|
+
|
|
301
|
+
export const handle = sequence(editor, version /* , ...the rest */)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```svelte
|
|
305
|
+
<!-- +layout.svelte -->
|
|
306
|
+
<script lang="ts">
|
|
307
|
+
import { EditBar } from 'storyloco/editor'
|
|
308
|
+
</script>
|
|
309
|
+
|
|
310
|
+
<EditBar />
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// app.d.ts
|
|
315
|
+
interface Locals {
|
|
316
|
+
editor?: import('storyloco/editor/server').Editor
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
A site with a Content Security Policy needs `wss://moxy.uilo.co` in its `connect-src`, for the
|
|
321
|
+
multiplayer socket, and `'self'` in its `frame-ancestors`, for the phone and tablet preview.
|
|
322
|
+
|
|
323
|
+
A site that picks its own content version rather than using `version` asks `editor_version(event)`
|
|
324
|
+
first: draft for editors and guests, published for the live view. A site that caches pages at
|
|
325
|
+
its CDN (`s-maxage`) needs editors, and guests (`sl_guest…`), on a cache key of their own, or
|
|
326
|
+
whoever signs in on the live site is handed the cached visitor page, with no edit bar. The CDN
|
|
327
|
+
answers before the hook runs and won't vary on cookies, so it takes Routing Middleware: when a
|
|
328
|
+
request carries an `sl_editor…` cookie, rewrite it onto a `?_storyloco=1` URL, and never cache
|
|
329
|
+
responses there.
|
|
330
|
+
|
|
331
|
+
No secrets: sign-in runs through moxy, and the space comes from `STORYBLOK_SPACE_ID` or the
|
|
332
|
+
site's public token. Blocks that spread `attrs(blok)` are editable as they are; nothing marks
|
|
333
|
+
fields up. With edit mode on (the badge's switch, or **E**):
|
|
334
|
+
|
|
335
|
+
- **Text** edits where it is, matched to its field by what it shows. Body copy edits in place
|
|
336
|
+
as rich text, with the site's own styles.
|
|
337
|
+
- **Images** offer Replace: the space's library or an upload, with alt text and a focus point.
|
|
338
|
+
- **Blocks** get a toolbar on hover to move, duplicate, add after or delete (with Undo), and a
|
|
339
|
+
click opens every field of the block from its schema, nested blocks included. Drag a block
|
|
340
|
+
by its name in the toolbar to put it anywhere in its list.
|
|
341
|
+
- **AI** suggests an image's alt text in the image picker, and rewrites a selection or
|
|
342
|
+
paragraph of rich text shorter, friendlier, clearer, or with its spelling fixed.
|
|
343
|
+
- **Other stories** shown on the page, like cards listing the latest articles, edit as those
|
|
344
|
+
stories: their text and images where they show, and a click opens them.
|
|
345
|
+
- **Video** uploads to Mux when the space has the Mux app connected.
|
|
346
|
+
- **Undo** takes back any change made from the page, and Redo makes it again: **⌘Z** and
|
|
347
|
+
**⇧⌘Z** (Ctrl+Z and Ctrl+Y) outside a field, or the badge's arrows.
|
|
348
|
+
- **Someone else** saving the page in Storyblok while it's open shows a note with Refresh.
|
|
349
|
+
- **⌘K** (Ctrl+K), or the badge's ⌘ button, finds a page, a block on this one, or something
|
|
350
|
+
to do: make a new page, ask someone to review this one, or preview it at phone or tablet size.
|
|
351
|
+
- **History** goes back through the page's saves on a timeline, the page showing itself as it
|
|
352
|
+
was at each one, and restores any of them as the draft (with Undo). Pages that `connect()`
|
|
353
|
+
their story show it in place; others list what restoring would change.
|
|
354
|
+
- **Compare with live** (⌘K, or from Publish) flips the page between the draft and what
|
|
355
|
+
visitors see now, scrolled to the same place (**L**), with the blocks that aren't live yet
|
|
356
|
+
marked on the draft.
|
|
357
|
+
- **Share** the draft, like a Google Doc, with people who don't edit in Storyblok: invite them
|
|
358
|
+
by email (they sign in from the link, once), or let anyone with the link look. Guests see the
|
|
359
|
+
draft and, if they can comment, comment; someone from the link confirms their email with a
|
|
360
|
+
code first, so no comment is anonymous. Guests never edit, resolve, or get a Storyblok token,
|
|
361
|
+
and their pages aren't cached or indexed.
|
|
362
|
+
|
|
363
|
+
Everyone editing a site at once sees each other. Each person has a colour: their pointer and
|
|
364
|
+
name move over the page, what they've selected is outlined, and their avatars sit by the
|
|
365
|
+
badge. Text and rich text type together, letters merging as they're typed with everyone's
|
|
366
|
+
caret showing, and saves to the same story take turns, so no one's change is lost.
|
|
367
|
+
|
|
368
|
+
- **Follow** someone by clicking their avatar: the page goes where they go and scrolls with
|
|
369
|
+
them. Scroll yourself, or press Escape, to stop.
|
|
370
|
+
- **Say something** by your pointer with **/**; the others see it as it's typed.
|
|
371
|
+
- **Comment** with **C**, or the badge's speech bubble, then click where it goes. Threads take
|
|
372
|
+
replies, @mentions and Resolve. Anyone in one, or mentioned, finds it under the badge's bell,
|
|
373
|
+
its unread count on the badge, and gets a browser notification too once they've chosen
|
|
374
|
+
Notify me. A review request lands there too, and anything left unread for 15 minutes is
|
|
375
|
+
emailed, guests' included.
|
|
376
|
+
|
|
377
|
+
Visitors never see any of it. On the live site, press **⌘⇧E** (Ctrl+Shift+E) or add `?edit` to
|
|
378
|
+
any address to sign in; once signed in, the badge shows on every page, and ⌘⇧E or **E** turns
|
|
379
|
+
edit mode on and off. In development the badge is always there.
|
|
380
|
+
|
|
381
|
+
Links inside content don't navigate in edit mode; hold ⌘ or Ctrl to follow one. Match the
|
|
382
|
+
editor to a site with `--editor-bg`, `-ink`, `-muted`, `-line`, `-shadow`, `-font`,
|
|
383
|
+
`--editor-accent` (and `-accent-ink`), `--editor-good`, `--editor-danger`, and `--editor-pill`
|
|
384
|
+
for the toast and the format bar, or build a bar of your own from `editor`, `set_editing`,
|
|
385
|
+
`publish`, `signin` and `signout`.
|
|
386
|
+
|
|
387
|
+
`bun run smoke <site url>…` (in `packages/vite`) checks each site loads and, signed in, that a
|
|
388
|
+
click on a block opens its settings. Sign in once per site with `--signin`, which opens a
|
|
389
|
+
window; later runs reuse it without one.
|
|
390
|
+
|
|
273
391
|
### Use Shared Components
|
|
274
392
|
|
|
275
393
|
```typescript
|
|
@@ -565,6 +683,13 @@ MIT License - feel free to use this however you want, you absolute legend! 🎯
|
|
|
565
683
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
566
684
|
5. Open a Pull Request
|
|
567
685
|
|
|
686
|
+
### Releasing
|
|
687
|
+
|
|
688
|
+
A release is a merge. Label a PR `release:patch`, `release:minor` or `release:major` before merging
|
|
689
|
+
it, and the Release workflow bumps the version, checks and builds it, commits "Release X.Y.Z: <the
|
|
690
|
+
PR's title>" to main with its tag, and publishes it to npm through trusted publishing, with no
|
|
691
|
+
token or login. To release main as it stands, run Actions → Release → Run workflow.
|
|
692
|
+
|
|
568
693
|
---
|
|
569
694
|
|
|
570
695
|
Built with ❤️ and a lot of swearing by the Storyloco team
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storyloco",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, and Tailwind CSS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"storyblok",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"packages/seo/types.ts",
|
|
37
37
|
"packages/hubspot_form/types.ts",
|
|
38
38
|
"packages/currency/types.ts",
|
|
39
|
-
"packages/dates/types.ts"
|
|
39
|
+
"packages/dates/types.ts",
|
|
40
|
+
"packages/churchsuite_categories/types.ts"
|
|
40
41
|
],
|
|
41
42
|
"exports": {
|
|
42
43
|
".": {
|
|
@@ -83,6 +84,19 @@
|
|
|
83
84
|
"types": "./packages/vite/dist/server.d.ts",
|
|
84
85
|
"import": "./packages/vite/dist/server.js"
|
|
85
86
|
},
|
|
87
|
+
"./editor": {
|
|
88
|
+
"types": "./packages/vite/dist/editor/index.d.ts",
|
|
89
|
+
"svelte": "./packages/vite/dist/editor/index.js",
|
|
90
|
+
"import": "./packages/vite/dist/editor/index.js"
|
|
91
|
+
},
|
|
92
|
+
"./middleware": {
|
|
93
|
+
"types": "./packages/vite/dist/middleware.d.ts",
|
|
94
|
+
"import": "./packages/vite/dist/middleware.js"
|
|
95
|
+
},
|
|
96
|
+
"./editor/server": {
|
|
97
|
+
"types": "./packages/vite/dist/editor/server.d.ts",
|
|
98
|
+
"import": "./packages/vite/dist/editor/server.js"
|
|
99
|
+
},
|
|
86
100
|
"./time": {
|
|
87
101
|
"types": "./packages/time/types.ts",
|
|
88
102
|
"svelte": "./packages/time/src/app.svelte.ts"
|
|
@@ -105,22 +119,25 @@
|
|
|
105
119
|
"./dates": {
|
|
106
120
|
"types": "./packages/dates/types.ts",
|
|
107
121
|
"svelte": "./packages/dates/src/app.svelte.ts"
|
|
122
|
+
},
|
|
123
|
+
"./churchsuite_categories": {
|
|
124
|
+
"types": "./packages/churchsuite_categories/types.ts"
|
|
108
125
|
}
|
|
109
126
|
},
|
|
110
127
|
"devDependencies": {
|
|
111
|
-
"@types/node": "26.
|
|
112
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
113
|
-
"@typescript-eslint/parser": "8.
|
|
114
|
-
"kitto": "1.
|
|
115
|
-
"oxfmt": "0.
|
|
128
|
+
"@types/node": "26.6.5",
|
|
129
|
+
"@typescript-eslint/eslint-plugin": "8.72.0",
|
|
130
|
+
"@typescript-eslint/parser": "8.72.0",
|
|
131
|
+
"kitto": "1.16.1",
|
|
132
|
+
"oxfmt": "0.74.0",
|
|
116
133
|
"typescript": "6.0.3",
|
|
117
|
-
"vite": "
|
|
134
|
+
"vite": "8.3.5"
|
|
118
135
|
},
|
|
119
136
|
"peerDependencies": {
|
|
120
137
|
"@storyblok/svelte": "7.2.2",
|
|
121
138
|
"@storyblok/field-plugin": "^1.7.1",
|
|
122
139
|
"svelte": "^5.0.0",
|
|
123
|
-
"@sveltejs/kit": "^2.0.0",
|
|
140
|
+
"@sveltejs/kit": "^2.0.0 || ^3.0.0",
|
|
124
141
|
"storyblok-js-client": "^7.0.0"
|
|
125
142
|
},
|
|
126
143
|
"scripts": {
|
|
@@ -134,7 +151,34 @@
|
|
|
134
151
|
"packages/*"
|
|
135
152
|
],
|
|
136
153
|
"dependencies": {
|
|
154
|
+
"@tiptap/core": "^3.32.0",
|
|
155
|
+
"@tiptap/extension-blockquote": "^3.32.0",
|
|
156
|
+
"@tiptap/extension-bold": "^3.32.0",
|
|
157
|
+
"@tiptap/extension-code": "^3.32.0",
|
|
158
|
+
"@tiptap/extension-code-block": "^3.32.0",
|
|
159
|
+
"@tiptap/extension-collaboration": "^3.32.0",
|
|
160
|
+
"@tiptap/extension-collaboration-caret": "^3.32.0",
|
|
161
|
+
"@tiptap/extension-document": "^3.32.0",
|
|
162
|
+
"@tiptap/extension-hard-break": "^3.32.0",
|
|
163
|
+
"@tiptap/extension-heading": "^3.32.0",
|
|
164
|
+
"@tiptap/extension-highlight": "^3.32.0",
|
|
165
|
+
"@tiptap/extension-horizontal-rule": "^3.32.0",
|
|
166
|
+
"@tiptap/extension-image": "^3.32.0",
|
|
167
|
+
"@tiptap/extension-italic": "^3.32.0",
|
|
168
|
+
"@tiptap/extension-link": "^3.32.0",
|
|
169
|
+
"@tiptap/extension-list": "^3.32.0",
|
|
170
|
+
"@tiptap/extension-paragraph": "^3.32.0",
|
|
171
|
+
"@tiptap/extension-strike": "^3.32.0",
|
|
172
|
+
"@tiptap/extension-subscript": "^3.32.0",
|
|
173
|
+
"@tiptap/extension-superscript": "^3.32.0",
|
|
174
|
+
"@tiptap/extension-table": "^3.32.0",
|
|
175
|
+
"@tiptap/extension-text": "^3.32.0",
|
|
176
|
+
"@tiptap/extension-underline": "^3.32.0",
|
|
177
|
+
"@tiptap/pm": "^3.32.0",
|
|
178
|
+
"@tiptap/y-tiptap": "^3.0.10",
|
|
137
179
|
"ora": "9.4.1",
|
|
138
|
-
"path-to-regexp": "
|
|
180
|
+
"path-to-regexp": "9.0.0",
|
|
181
|
+
"y-protocols": "^1.0.7",
|
|
182
|
+
"yjs": "^13.6.33"
|
|
139
183
|
}
|
|
140
184
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ChurchsuiteCategory {
|
|
2
|
+
id: number
|
|
3
|
+
name: string
|
|
4
|
+
colour?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// the chosen ChurchSuite calendar category ids; the same shape the legacy
|
|
8
|
+
// churchsuite-event-categories plugin saved, so old values load as they are
|
|
9
|
+
export type ChurchsuiteCategories = Array<number>
|
|
@@ -1,52 +1,52 @@
|
|
|
1
1
|
import { type WithElementRef } from '../../../utils.js';
|
|
2
2
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
|
3
3
|
import { type VariantProps } from 'tailwind-variants';
|
|
4
|
-
export declare const variants: import("tailwind-variants").TVReturnType<{
|
|
4
|
+
export declare const variants: import("tailwind-variants/dist/types.cjs").TVReturnType<{
|
|
5
5
|
variant: {
|
|
6
|
-
default:
|
|
7
|
-
destructive:
|
|
8
|
-
outline:
|
|
9
|
-
secondary:
|
|
10
|
-
ghost:
|
|
11
|
-
link:
|
|
6
|
+
default: "bg-tertiary text-tertiary-foreground shadow-xs hover:bg-tertiary/90";
|
|
7
|
+
destructive: "bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white";
|
|
8
|
+
outline: "bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border";
|
|
9
|
+
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80 border";
|
|
10
|
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-secondary-foreground/10";
|
|
11
|
+
link: "text-primary underline-offset-4 hover:underline";
|
|
12
12
|
};
|
|
13
13
|
size: {
|
|
14
|
-
default:
|
|
15
|
-
sm:
|
|
16
|
-
lg:
|
|
17
|
-
icon:
|
|
14
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3";
|
|
15
|
+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5";
|
|
16
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4";
|
|
17
|
+
icon: "size-9";
|
|
18
18
|
};
|
|
19
19
|
}, undefined, "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 relative cursor-pointer", {
|
|
20
20
|
variant: {
|
|
21
|
-
default:
|
|
22
|
-
destructive:
|
|
23
|
-
outline:
|
|
24
|
-
secondary:
|
|
25
|
-
ghost:
|
|
26
|
-
link:
|
|
21
|
+
default: "bg-tertiary text-tertiary-foreground shadow-xs hover:bg-tertiary/90";
|
|
22
|
+
destructive: "bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white";
|
|
23
|
+
outline: "bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border";
|
|
24
|
+
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80 border";
|
|
25
|
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-secondary-foreground/10";
|
|
26
|
+
link: "text-primary underline-offset-4 hover:underline";
|
|
27
27
|
};
|
|
28
28
|
size: {
|
|
29
|
-
default:
|
|
30
|
-
sm:
|
|
31
|
-
lg:
|
|
32
|
-
icon:
|
|
29
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3";
|
|
30
|
+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5";
|
|
31
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4";
|
|
32
|
+
icon: "size-9";
|
|
33
33
|
};
|
|
34
|
-
}, undefined, import("tailwind-variants").
|
|
34
|
+
}, undefined, import("tailwind-variants/dist/types.cjs").TVReturnTypeLike<{
|
|
35
35
|
variant: {
|
|
36
|
-
default:
|
|
37
|
-
destructive:
|
|
38
|
-
outline:
|
|
39
|
-
secondary:
|
|
40
|
-
ghost:
|
|
41
|
-
link:
|
|
36
|
+
default: "bg-tertiary text-tertiary-foreground shadow-xs hover:bg-tertiary/90";
|
|
37
|
+
destructive: "bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white";
|
|
38
|
+
outline: "bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border";
|
|
39
|
+
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80 border";
|
|
40
|
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-secondary-foreground/10";
|
|
41
|
+
link: "text-primary underline-offset-4 hover:underline";
|
|
42
42
|
};
|
|
43
43
|
size: {
|
|
44
|
-
default:
|
|
45
|
-
sm:
|
|
46
|
-
lg:
|
|
47
|
-
icon:
|
|
44
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3";
|
|
45
|
+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5";
|
|
46
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4";
|
|
47
|
+
icon: "size-9";
|
|
48
48
|
};
|
|
49
|
-
}, undefined
|
|
49
|
+
}, undefined>>;
|
|
50
50
|
export type ButtonVariant = VariantProps<typeof variants>['variant'];
|
|
51
51
|
export type ButtonSize = VariantProps<typeof variants>['size'];
|
|
52
52
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> & WithElementRef<HTMLAnchorAttributes> & {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a comment's pin sits inside its block, finer than a fraction of the block: the element it's on, by
|
|
3
|
+
* its place under the block, and the character when it's on text. A block reflows at another width, its
|
|
4
|
+
* text rewrapping, and the pin follows the word it was put on.
|
|
5
|
+
*/
|
|
6
|
+
export type Anchor = {
|
|
7
|
+
/** Child indexes from the block down to the element */
|
|
8
|
+
path: Array<number>;
|
|
9
|
+
tag: string;
|
|
10
|
+
/** Where on the element, or on the character, as a fraction of its box */
|
|
11
|
+
fx: number;
|
|
12
|
+
fy: number;
|
|
13
|
+
/** The character, as an offset into the element's text, and the text from there to find it again by */
|
|
14
|
+
at?: number;
|
|
15
|
+
near?: string;
|
|
16
|
+
};
|
|
17
|
+
/** The anchor for a point on an element inside a block; null when it isn't inside */
|
|
18
|
+
export declare function anchor_at(block: Element, under: Element, x: number, y: number): Anchor | null;
|
|
19
|
+
/** Where an anchor is on screen now; null when the block's changed so it isn't there any more */
|
|
20
|
+
export declare function anchored(block: Element, anchor: Anchor): {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
} | null;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const NEAR = 16;
|
|
2
|
+
const within = (box, x, y) => x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
|
|
3
|
+
/** The character under a point among an element's text, as its text node and offset */
|
|
4
|
+
function character_at(el, x, y) {
|
|
5
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
6
|
+
const range = document.createRange();
|
|
7
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
8
|
+
range.selectNodeContents(node);
|
|
9
|
+
if (![...range.getClientRects()].some((box) => within(box, x, y)))
|
|
10
|
+
continue;
|
|
11
|
+
for (let i = 0; i < node.length; i++) {
|
|
12
|
+
range.setStart(node, i);
|
|
13
|
+
range.setEnd(node, i + 1);
|
|
14
|
+
const box = [...range.getClientRects()].find((b) => within(b, x, y));
|
|
15
|
+
if (box)
|
|
16
|
+
return { node, i, box };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/** The anchor for a point on an element inside a block; null when it isn't inside */
|
|
22
|
+
export function anchor_at(block, under, x, y) {
|
|
23
|
+
if (!block.contains(under))
|
|
24
|
+
return null;
|
|
25
|
+
const hit = character_at(under, x, y);
|
|
26
|
+
const el = hit?.node.parentElement ?? under;
|
|
27
|
+
const path = [];
|
|
28
|
+
for (let at = el; at !== block; at = at.parentElement)
|
|
29
|
+
path.unshift([...at.parentElement.children].indexOf(at));
|
|
30
|
+
let on = null;
|
|
31
|
+
if (hit) {
|
|
32
|
+
const before = document.createRange();
|
|
33
|
+
before.setStart(el, 0);
|
|
34
|
+
before.setEnd(hit.node, hit.i);
|
|
35
|
+
const at = before.toString().length;
|
|
36
|
+
const near = (el.textContent ?? "").slice(at, at + NEAR);
|
|
37
|
+
// all space is nothing to find it by
|
|
38
|
+
if (near.trim())
|
|
39
|
+
on = { at, near };
|
|
40
|
+
}
|
|
41
|
+
const box = on ? hit.box : el.getBoundingClientRect();
|
|
42
|
+
return {
|
|
43
|
+
path,
|
|
44
|
+
tag: el.localName,
|
|
45
|
+
fx: box.width ? (x - box.left) / box.width : 0,
|
|
46
|
+
fy: box.height ? (y - box.top) / box.height : 0,
|
|
47
|
+
...on,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Where an anchor is on screen now; null when the block's changed so it isn't there any more */
|
|
51
|
+
export function anchored(block, anchor) {
|
|
52
|
+
let el = block;
|
|
53
|
+
for (const i of anchor.path)
|
|
54
|
+
el = el?.children[i];
|
|
55
|
+
if (!el || el.localName !== anchor.tag)
|
|
56
|
+
return null;
|
|
57
|
+
if (anchor.at === undefined || !anchor.near) {
|
|
58
|
+
const box = el.getBoundingClientRect();
|
|
59
|
+
if (!box.width && !box.height)
|
|
60
|
+
return null;
|
|
61
|
+
return { x: box.left + anchor.fx * box.width, y: box.top + anchor.fy * box.height };
|
|
62
|
+
}
|
|
63
|
+
// the same words, where they were or as near there as they've moved to with an edit
|
|
64
|
+
const text = el.textContent ?? "";
|
|
65
|
+
const after = text.indexOf(anchor.near, anchor.at);
|
|
66
|
+
const before = text.lastIndexOf(anchor.near, anchor.at);
|
|
67
|
+
const at = after < 0
|
|
68
|
+
? before
|
|
69
|
+
: before < 0
|
|
70
|
+
? after
|
|
71
|
+
: after - anchor.at < anchor.at - before
|
|
72
|
+
? after
|
|
73
|
+
: before;
|
|
74
|
+
if (at < 0)
|
|
75
|
+
return null;
|
|
76
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
77
|
+
let left = at;
|
|
78
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
79
|
+
if (left >= node.length) {
|
|
80
|
+
left -= node.length;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const range = document.createRange();
|
|
84
|
+
range.setStart(node, left);
|
|
85
|
+
range.setEnd(node, left + 1);
|
|
86
|
+
const box = range.getClientRects()[0];
|
|
87
|
+
if (!box)
|
|
88
|
+
return null;
|
|
89
|
+
return { x: box.left + anchor.fx * box.width, y: box.top + anchor.fy * box.height };
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|