@setzkasten-cms/ui 0.4.2

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.
@@ -0,0 +1,41 @@
1
+ import { memo, useState } from 'react'
2
+ import type { ObjectFieldDef, AnyFieldDef } from '@setzkasten-cms/core'
3
+ import { FieldRenderer, type FieldRendererProps } from './field-renderer'
4
+
5
+ export const ObjectFieldRenderer = memo(function ObjectFieldRenderer({
6
+ field,
7
+ path,
8
+ store,
9
+ }: FieldRendererProps) {
10
+ const objField = field as ObjectFieldDef
11
+ const [collapsed, setCollapsed] = useState(false)
12
+
13
+ return (
14
+ <div className="sk-field sk-field--object">
15
+ <div className="sk-field__object-header">
16
+ <label className="sk-field__label">{objField.label}</label>
17
+ {objField.collapsible && (
18
+ <button
19
+ type="button"
20
+ className="sk-field__object-toggle"
21
+ onClick={() => setCollapsed(!collapsed)}
22
+ >
23
+ {collapsed ? '▸' : '▾'}
24
+ </button>
25
+ )}
26
+ </div>
27
+ {!collapsed && (
28
+ <div className="sk-field__object-fields">
29
+ {Object.entries(objField.fields).map(([key, childField]) => (
30
+ <FieldRenderer
31
+ key={key}
32
+ field={childField as AnyFieldDef}
33
+ path={[...path, key]}
34
+ store={store}
35
+ />
36
+ ))}
37
+ </div>
38
+ )}
39
+ </div>
40
+ )
41
+ })
@@ -0,0 +1,48 @@
1
+ import { memo } from 'react'
2
+ import type { OverrideFieldDef, AnyFieldDef } from '@setzkasten-cms/core'
3
+ import { useField } from '../hooks/use-field'
4
+ import { FieldRenderer, type FieldRendererProps } from './field-renderer'
5
+
6
+ /**
7
+ * Override field – native replacement for Keystatic's DOM-hack.
8
+ * Shows an "active" checkbox. When unchecked, child fields are hidden.
9
+ */
10
+ export const OverrideFieldRenderer = memo(function OverrideFieldRenderer({
11
+ field,
12
+ path,
13
+ store,
14
+ }: FieldRendererProps) {
15
+ const overrideField = field as OverrideFieldDef
16
+ const { value, setValue } = useField(store, path)
17
+
18
+ const overrideValue = (value as { active: boolean } | undefined) ?? { active: false }
19
+ const isActive = overrideValue.active
20
+
21
+ const toggleActive = () => {
22
+ setValue({ ...overrideValue, active: !isActive })
23
+ }
24
+
25
+ return (
26
+ <div className="sk-field sk-field--override" data-active={isActive || undefined}>
27
+ <div className="sk-field__override-header">
28
+ <label className="sk-field__toggle">
29
+ <input type="checkbox" checked={isActive} onChange={toggleActive} />
30
+ <span className="sk-field__toggle-label">{overrideField.label}</span>
31
+ </label>
32
+ </div>
33
+
34
+ {isActive && (
35
+ <div className="sk-field__override-fields">
36
+ {Object.entries(overrideField.fields).map(([key, childField]) => (
37
+ <FieldRenderer
38
+ key={key}
39
+ field={childField as AnyFieldDef}
40
+ path={[...path, key]}
41
+ store={store}
42
+ />
43
+ ))}
44
+ </div>
45
+ )}
46
+ </div>
47
+ )
48
+ })
@@ -0,0 +1,42 @@
1
+ import { memo } from 'react'
2
+ import type { SelectFieldDef } from '@setzkasten-cms/core'
3
+ import { useField } from '../hooks/use-field'
4
+ import type { FieldRendererProps } from './field-renderer'
5
+
6
+ export const SelectFieldRenderer = memo(function SelectFieldRenderer({
7
+ field,
8
+ path,
9
+ store,
10
+ }: FieldRendererProps) {
11
+ const selectField = field as SelectFieldDef
12
+ const { value, errors, setValue, touch } = useField(store, path)
13
+
14
+ return (
15
+ <div className="sk-field sk-field--select">
16
+ <label className="sk-field__label">{selectField.label}</label>
17
+ {selectField.description && (
18
+ <p className="sk-field__description">{selectField.description}</p>
19
+ )}
20
+ <select
21
+ className="sk-field__input sk-field__select"
22
+ value={(value as string) ?? ''}
23
+ onChange={(e) => setValue(e.target.value)}
24
+ onBlur={touch}
25
+ aria-label={selectField.label}
26
+ >
27
+ {selectField.options.map((opt) => (
28
+ <option key={opt.value} value={opt.value}>
29
+ {opt.label}
30
+ </option>
31
+ ))}
32
+ </select>
33
+ {errors.length > 0 && (
34
+ <div className="sk-field__errors">
35
+ {errors.map((err, i) => (
36
+ <p key={i} className="sk-field__error">{err}</p>
37
+ ))}
38
+ </div>
39
+ )}
40
+ </div>
41
+ )
42
+ })
@@ -0,0 +1,313 @@
1
+ import { memo, useCallback, useEffect, useRef, useState } from 'react'
2
+ import type { TextFieldDef } from '@setzkasten-cms/core'
3
+ import { useField } from '../hooks/use-field'
4
+ import type { FieldRendererProps } from './field-renderer'
5
+ import { useEditor, EditorContent } from '@tiptap/react'
6
+ import StarterKit from '@tiptap/starter-kit'
7
+ import Placeholder from '@tiptap/extension-placeholder'
8
+ import Link from '@tiptap/extension-link'
9
+ import TextAlign from '@tiptap/extension-text-align'
10
+ import {
11
+ Type,
12
+ AlignLeft,
13
+ AlignCenter,
14
+ AlignRight,
15
+ AlignJustify,
16
+ } from 'lucide-react'
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // localStorage helpers for formatting toggle persistence
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const STORAGE_KEY = 'sk-formatting-fields'
23
+
24
+ function getFormattingState(fieldPath: string): boolean {
25
+ try {
26
+ const stored = localStorage.getItem(STORAGE_KEY)
27
+ if (!stored) return false
28
+ const map = JSON.parse(stored) as Record<string, boolean>
29
+ return map[fieldPath] ?? false
30
+ } catch {
31
+ return false
32
+ }
33
+ }
34
+
35
+ function setFormattingState(fieldPath: string, enabled: boolean) {
36
+ try {
37
+ const stored = localStorage.getItem(STORAGE_KEY)
38
+ const map = stored ? (JSON.parse(stored) as Record<string, boolean>) : {}
39
+ if (enabled) {
40
+ map[fieldPath] = true
41
+ } else {
42
+ delete map[fieldPath]
43
+ }
44
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(map))
45
+ } catch {
46
+ // ignore
47
+ }
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Inline toolbar button (shared)
52
+ // ---------------------------------------------------------------------------
53
+
54
+ const InlineToolbarButton = memo(function InlineToolbarButton({
55
+ label,
56
+ isActive,
57
+ onClick,
58
+ title,
59
+ children,
60
+ }: {
61
+ label?: string
62
+ isActive: boolean
63
+ onClick: () => void
64
+ title?: string
65
+ children?: React.ReactNode
66
+ }) {
67
+ return (
68
+ <button
69
+ type="button"
70
+ className={`sk-rte__btn${isActive ? ' sk-rte__btn--active' : ''}`}
71
+ onClick={onClick}
72
+ title={title ?? label}
73
+ >
74
+ {children ?? label}
75
+ </button>
76
+ )
77
+ })
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Mini rich-text editor for inline formatting
81
+ // ---------------------------------------------------------------------------
82
+
83
+ const FormattedTextInput = memo(function FormattedTextInput({
84
+ field,
85
+ path,
86
+ store,
87
+ }: FieldRendererProps) {
88
+ const textField = field as TextFieldDef
89
+ const { value, errors, isDirty, setValue, touch } = useField(store, path)
90
+ const isUpdatingRef = useRef(false)
91
+
92
+ const editor = useEditor({
93
+ extensions: [
94
+ StarterKit.configure({
95
+ heading: false,
96
+ horizontalRule: false,
97
+ codeBlock: false,
98
+ }),
99
+ Placeholder.configure({ placeholder: textField.placeholder ?? 'Schreibe hier...' }),
100
+ Link.configure({
101
+ openOnClick: false,
102
+ HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' },
103
+ }),
104
+ TextAlign.configure({ types: ['paragraph'] }),
105
+ ],
106
+ content: (value as string) || '',
107
+ onUpdate: ({ editor: ed }) => {
108
+ isUpdatingRef.current = true
109
+ setValue(ed.getHTML())
110
+ isUpdatingRef.current = false
111
+ },
112
+ onBlur: () => touch(),
113
+ })
114
+
115
+ useEffect(() => {
116
+ if (!editor || isUpdatingRef.current) return
117
+ const current = editor.getHTML()
118
+ if (current !== value && typeof value === 'string') {
119
+ editor.commands.setContent(value, { emitUpdate: false })
120
+ }
121
+ }, [editor, value])
122
+
123
+ const handleLink = useCallback(() => {
124
+ if (!editor) return
125
+ if (editor.isActive('link')) {
126
+ editor.chain().focus().unsetLink().run()
127
+ return
128
+ }
129
+ const url = window.prompt('URL eingeben:')
130
+ if (!url) return
131
+ editor.chain().focus().setLink({ href: url }).run()
132
+ }, [editor])
133
+
134
+ if (!editor) return null
135
+
136
+ return (
137
+ <div className="sk-rte sk-rte--inline">
138
+ <div className="sk-rte__toolbar">
139
+ <InlineToolbarButton
140
+ label="B"
141
+ isActive={editor.isActive('bold')}
142
+ onClick={() => { editor.chain().focus().toggleBold().run() }}
143
+ />
144
+ <InlineToolbarButton
145
+ label="I"
146
+ isActive={editor.isActive('italic')}
147
+ onClick={() => { editor.chain().focus().toggleItalic().run() }}
148
+ />
149
+ <InlineToolbarButton
150
+ label="S"
151
+ isActive={editor.isActive('strike')}
152
+ onClick={() => { editor.chain().focus().toggleStrike().run() }}
153
+ />
154
+ <InlineToolbarButton
155
+ label="🔗"
156
+ isActive={editor.isActive('link')}
157
+ onClick={handleLink}
158
+ title="Link"
159
+ />
160
+ <span className="sk-rte__sep" />
161
+ <InlineToolbarButton
162
+ label="&bull;"
163
+ isActive={editor.isActive('bulletList')}
164
+ onClick={() => { editor.chain().focus().toggleBulletList().run() }}
165
+ />
166
+ <InlineToolbarButton
167
+ label="1."
168
+ isActive={editor.isActive('orderedList')}
169
+ onClick={() => { editor.chain().focus().toggleOrderedList().run() }}
170
+ />
171
+ <InlineToolbarButton
172
+ label="&ldquo;"
173
+ isActive={editor.isActive('blockquote')}
174
+ onClick={() => { editor.chain().focus().toggleBlockquote().run() }}
175
+ />
176
+ <span className="sk-rte__sep" />
177
+ <InlineToolbarButton
178
+ isActive={editor.isActive({ textAlign: 'left' })}
179
+ onClick={() => { editor.chain().focus().setTextAlign('left').run() }}
180
+ title="Linksbündig"
181
+ >
182
+ <AlignLeft size={14} />
183
+ </InlineToolbarButton>
184
+ <InlineToolbarButton
185
+ isActive={editor.isActive({ textAlign: 'center' })}
186
+ onClick={() => { editor.chain().focus().setTextAlign('center').run() }}
187
+ title="Zentriert"
188
+ >
189
+ <AlignCenter size={14} />
190
+ </InlineToolbarButton>
191
+ <InlineToolbarButton
192
+ isActive={editor.isActive({ textAlign: 'right' })}
193
+ onClick={() => { editor.chain().focus().setTextAlign('right').run() }}
194
+ title="Rechtsbündig"
195
+ >
196
+ <AlignRight size={14} />
197
+ </InlineToolbarButton>
198
+ <InlineToolbarButton
199
+ isActive={editor.isActive({ textAlign: 'justify' })}
200
+ onClick={() => { editor.chain().focus().setTextAlign('justify').run() }}
201
+ title="Blocksatz"
202
+ >
203
+ <AlignJustify size={14} />
204
+ </InlineToolbarButton>
205
+ </div>
206
+ <EditorContent editor={editor} className="sk-rte__content sk-rte__content--inline" />
207
+ </div>
208
+ )
209
+ })
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Plain text input (default)
213
+ // ---------------------------------------------------------------------------
214
+
215
+ const PlainTextInput = memo(function PlainTextInput({
216
+ field,
217
+ path,
218
+ store,
219
+ }: FieldRendererProps) {
220
+ const textField = field as TextFieldDef
221
+ const { value, errors, setValue, touch } = useField(store, path)
222
+
223
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
224
+ setValue(e.target.value)
225
+ }
226
+
227
+ const inputProps = {
228
+ value: (value as string) ?? '',
229
+ onChange: handleChange,
230
+ onBlur: touch,
231
+ placeholder: textField.placeholder,
232
+ maxLength: textField.maxLength,
233
+ 'aria-label': textField.label,
234
+ 'aria-invalid': errors.length > 0,
235
+ }
236
+
237
+ return (
238
+ <>
239
+ {textField.multiline ? (
240
+ <textarea className="sk-field__input sk-field__textarea" rows={4} {...inputProps} />
241
+ ) : (
242
+ <input className="sk-field__input" type="text" {...inputProps} />
243
+ )}
244
+ {textField.maxLength && (
245
+ <span className="sk-field__counter">
246
+ {((value as string) ?? '').length}/{textField.maxLength}
247
+ </span>
248
+ )}
249
+ </>
250
+ )
251
+ })
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Main text field renderer with formatting toggle
255
+ // ---------------------------------------------------------------------------
256
+
257
+ export const TextFieldRenderer = memo(function TextFieldRenderer(props: FieldRendererProps) {
258
+ const textField = props.field as TextFieldDef
259
+ const { errors, isDirty } = useField(props.store, props.path)
260
+ const fieldPath = props.path.join('.')
261
+
262
+ // Schema can force formatting on; otherwise use localStorage toggle
263
+ const [formatting, setFormatting] = useState(() =>
264
+ textField.formatting || getFormattingState(fieldPath),
265
+ )
266
+
267
+ const toggleFormatting = useCallback(() => {
268
+ setFormatting((prev) => {
269
+ const next = !prev
270
+ setFormattingState(fieldPath, next)
271
+ return next
272
+ })
273
+ }, [fieldPath])
274
+
275
+ return (
276
+ <div
277
+ className={`sk-field sk-field--text${formatting ? ' sk-field--formatted' : ''}`}
278
+ data-dirty={isDirty || undefined}
279
+ >
280
+ <div className="sk-field__header">
281
+ <label className="sk-field__label">
282
+ {textField.label}
283
+ {textField.required && <span className="sk-field__required">*</span>}
284
+ </label>
285
+ <button
286
+ type="button"
287
+ className={`sk-field__format-toggle${formatting ? ' sk-field__format-toggle--active' : ''}`}
288
+ onClick={toggleFormatting}
289
+ title={formatting ? 'Formatierung ausschalten' : 'Formatierung einschalten'}
290
+ >
291
+ <Type size={14} />
292
+ </button>
293
+ </div>
294
+ {textField.description && (
295
+ <p className="sk-field__description">{textField.description}</p>
296
+ )}
297
+
298
+ {formatting ? (
299
+ <FormattedTextInput {...props} />
300
+ ) : (
301
+ <PlainTextInput {...props} />
302
+ )}
303
+
304
+ {errors.length > 0 && (
305
+ <div className="sk-field__errors">
306
+ {errors.map((err, i) => (
307
+ <p key={i} className="sk-field__error">{err}</p>
308
+ ))}
309
+ </div>
310
+ )}
311
+ </div>
312
+ )
313
+ })
@@ -0,0 +1,82 @@
1
+ import { useCallback, useMemo, useRef } from 'react'
2
+ import { useStore } from 'zustand'
3
+ import type { FieldPath } from '@setzkasten-cms/core'
4
+
5
+ // Stable empty array to avoid new references on every render
6
+ const EMPTY_ERRORS: string[] = []
7
+
8
+ // Path-based deep get (duplicated from form-store to avoid circular deps)
9
+ function getIn(obj: unknown, path: FieldPath): unknown {
10
+ let current: unknown = obj
11
+ for (const key of path) {
12
+ if (current === null || current === undefined) return undefined
13
+ if (typeof current === 'object') {
14
+ current = (current as Record<string | number, unknown>)[key]
15
+ } else {
16
+ return undefined
17
+ }
18
+ }
19
+ return current
20
+ }
21
+
22
+ /**
23
+ * Hook for individual field state – subscribes only to its own path.
24
+ * This prevents unnecessary re-renders when other fields change.
25
+ *
26
+ * IMPORTANT: All selectors must return referentially stable values
27
+ * to avoid React 19 useSyncExternalStore infinite loops.
28
+ */
29
+ export function useField(store: ReturnType<typeof import('../stores/form-store').createFormStore>, path: FieldPath) {
30
+ const pathKey = path.join('.')
31
+
32
+ // Use stable selector references
33
+ const value = useStore(store, useCallback(
34
+ (s) => getIn(s.values, path),
35
+ [pathKey],
36
+ ))
37
+
38
+ const errors = useStore(store, useCallback(
39
+ (s) => {
40
+ const fieldErrors = s.errors[pathKey]
41
+ return fieldErrors && fieldErrors.length > 0 ? fieldErrors : EMPTY_ERRORS
42
+ },
43
+ [pathKey],
44
+ ))
45
+
46
+ const isTouched = useStore(store, useCallback(
47
+ (s) => s.touched.has(pathKey),
48
+ [pathKey],
49
+ ))
50
+
51
+ // Compute isDirty outside of useStore to avoid unstable selector
52
+ const initialValues = useStore(store, useCallback(
53
+ (s) => getIn(s.initialValues, path),
54
+ [pathKey],
55
+ ))
56
+
57
+ const isDirty = JSON.stringify(value) !== JSON.stringify(initialValues)
58
+
59
+ const setValue = useCallback(
60
+ (newValue: unknown) => {
61
+ store.getState().setFieldValue(path, newValue)
62
+ },
63
+ [store, pathKey],
64
+ )
65
+
66
+ const touch = useCallback(() => {
67
+ store.getState().touchField(path)
68
+ }, [store, pathKey])
69
+
70
+ return useMemo(
71
+ () => ({
72
+ value,
73
+ errors,
74
+ isTouched,
75
+ isDirty,
76
+ setValue,
77
+ touch,
78
+ path,
79
+ }),
80
+ [value, errors, isTouched, isDirty, setValue, touch, path],
81
+ )
82
+ }
@@ -0,0 +1,46 @@
1
+ import { useCallback, useState } from 'react'
2
+ import type { Result, CommitResult } from '@setzkasten-cms/core'
3
+ import { useRepository, useEventBus } from '../providers/setzkasten-provider'
4
+ import type { createFormStore } from '../stores/form-store'
5
+
6
+ /**
7
+ * Hook for saving form content to the repository.
8
+ */
9
+ export function useSave(
10
+ store: ReturnType<typeof createFormStore>,
11
+ collection: string,
12
+ slug: string,
13
+ ) {
14
+ const repository = useRepository()
15
+ const eventBus = useEventBus()
16
+ const [saving, setSaving] = useState(false)
17
+
18
+ const save = useCallback(async (): Promise<Result<CommitResult>> => {
19
+ const state = store.getState()
20
+
21
+ if (!state.isDirty()) {
22
+ return { ok: true, value: { sha: '', message: 'No changes' } }
23
+ }
24
+
25
+ setSaving(true)
26
+ state.setStatus('saving')
27
+
28
+ const result = await repository.saveEntry(collection, slug, {
29
+ content: state.values,
30
+ })
31
+
32
+ if (result.ok) {
33
+ // Update initial values to current (no longer dirty)
34
+ state.init(state.schema!, state.values)
35
+ state.setStatus('idle')
36
+ eventBus.emit({ type: 'entry-saved', collection, slug })
37
+ } else {
38
+ state.setStatus('error', result.error.message)
39
+ }
40
+
41
+ setSaving(false)
42
+ return result
43
+ }, [store, repository, eventBus, collection, slug])
44
+
45
+ return { save, saving }
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @setzkasten-cms/ui – Public API
3
+ // ---------------------------------------------------------------------------
4
+
5
+ // Providers
6
+ export { SetzKastenProvider, useSetzKasten, useRepository, useAuth, useAssets, useEventBus, useConfig } from './providers/setzkasten-provider'
7
+ export type { SetzKastenContext } from './providers/setzkasten-provider'
8
+
9
+ // Stores
10
+ export { createFormStore } from './stores/form-store'
11
+ export type { FormState, FormActions, FormStore } from './stores/form-store'
12
+ export { createAppStore } from './stores/app-store'
13
+ export type { AppState, AppActions, AppStore, RecentEntry } from './stores/app-store'
14
+
15
+ // Fields
16
+ export { FieldRenderer } from './fields/field-renderer'
17
+ export type { FieldRendererProps } from './fields/field-renderer'
18
+
19
+ // Components
20
+ export { EntryForm } from './components/entry-form'
21
+ export { EntryList } from './components/entry-list'
22
+ export { CollectionView } from './components/collection-view'
23
+ export { AdminApp } from './components/admin-app'
24
+ export { ToastProvider, useToast } from './components/toast'
25
+
26
+ // Hooks
27
+ export { useField } from './hooks/use-field'
28
+ export { useSave } from './hooks/use-save'
29
+
30
+ // Adapters
31
+ export { ProxyContentRepository } from './adapters/proxy-content-repository'
32
+ export type { ProxyConfig } from './adapters/proxy-content-repository'
33
+ export { ProxyAssetStore } from './adapters/proxy-asset-store'
34
+ export type { ProxyAssetConfig } from './adapters/proxy-asset-store'
@@ -0,0 +1,80 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from 'react'
2
+ import type {
3
+ ContentRepository,
4
+ AuthProvider,
5
+ AssetStore,
6
+ SetzKastenConfig,
7
+ ContentEventBus,
8
+ } from '@setzkasten-cms/core'
9
+ import { createEventBus } from '@setzkasten-cms/core'
10
+
11
+ export interface SetzKastenContext {
12
+ config: SetzKastenConfig
13
+ repository: ContentRepository
14
+ auth: AuthProvider
15
+ assets: AssetStore
16
+ eventBus: ContentEventBus
17
+ }
18
+
19
+ const Context = createContext<SetzKastenContext | null>(null)
20
+
21
+ interface SetzKastenProviderProps {
22
+ config: SetzKastenConfig
23
+ repository: ContentRepository
24
+ auth: AuthProvider
25
+ assets: AssetStore
26
+ children: ReactNode
27
+ }
28
+
29
+ /**
30
+ * Root provider – the composition root injects all concrete adapters here.
31
+ * UI components access ports only via this context (Dependency Inversion).
32
+ */
33
+ export function SetzKastenProvider({
34
+ config,
35
+ repository,
36
+ auth,
37
+ assets,
38
+ children,
39
+ }: SetzKastenProviderProps) {
40
+ const value = useMemo(
41
+ () => ({
42
+ config,
43
+ repository,
44
+ auth,
45
+ assets,
46
+ eventBus: createEventBus(),
47
+ }),
48
+ [config, repository, auth, assets],
49
+ )
50
+
51
+ return <Context value={value}>{children}</Context>
52
+ }
53
+
54
+ export function useSetzKasten(): SetzKastenContext {
55
+ const context = useContext(Context)
56
+ if (!context) {
57
+ throw new Error('useSetzKasten must be used within a <SetzKastenProvider>')
58
+ }
59
+ return context
60
+ }
61
+
62
+ export function useRepository(): ContentRepository {
63
+ return useSetzKasten().repository
64
+ }
65
+
66
+ export function useAuth(): AuthProvider {
67
+ return useSetzKasten().auth
68
+ }
69
+
70
+ export function useAssets(): AssetStore {
71
+ return useSetzKasten().assets
72
+ }
73
+
74
+ export function useEventBus(): ContentEventBus {
75
+ return useSetzKasten().eventBus
76
+ }
77
+
78
+ export function useConfig(): SetzKastenConfig {
79
+ return useSetzKasten().config
80
+ }