@architecture-model/editable-metadata 0.2.0 → 0.3.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/Derived.astro ADDED
@@ -0,0 +1,17 @@
1
+ ---
2
+ import type { LikeC4Model } from 'likec4/model'
3
+ import { model } from './model.js'
4
+
5
+ type Value = string | number | boolean | null | undefined | Array<string | number>
6
+
7
+ interface Props {
8
+ of: string
9
+ value: (element: LikeC4Model.Element, model: LikeC4Model) => Value
10
+ }
11
+
12
+ const { of, value } = Astro.props
13
+ const result = value(model.element(of), model)
14
+ const text = typeof result === 'boolean' ? (result ? 'Yes' : 'No') : [result ?? []].flat().join(', ')
15
+ ---
16
+
17
+ {text}
package/Editor.jsx CHANGED
@@ -4,17 +4,25 @@ import { Command } from 'cmdk'
4
4
  import { useRef, useState } from 'react'
5
5
  import './editor.css'
6
6
 
7
- const post = async (fqn, key, value) => {
8
- const response = await fetch('/__model/metadata', { method: 'POST', body: JSON.stringify({ fqn, key, value }) })
7
+ const post = async (change) => {
8
+ const response = await fetch('/__model/element', { method: 'POST', body: JSON.stringify(change) })
9
9
  if (!response.ok) throw new Error(await response.text())
10
10
  }
11
11
 
12
12
  const listOf = (value) => [value].flat().filter(Boolean)
13
- const same = (a, b) => a.length === b.length && a.every((value) => b.includes(value))
13
+ const linksOf = (links) =>
14
+ links
15
+ .map(({ url, label }) => ({ url: url.trim(), label: label?.trim() }))
16
+ .filter((link) => link.url)
17
+ .map(({ url, label }) => (label ? { url, label } : { url }))
18
+ const keyOf = (list) => JSON.stringify(list.map((item) => JSON.stringify(item)).sort())
19
+ const shown = (type, values, labelOf) =>
20
+ type === 'links' ? values.map((link) => link.label || link.url).join(', ') : values.map(labelOf).join(', ')
14
21
 
15
22
  function Options({ type, options, chosen, onChoose, onClear }) {
16
23
  const [query, setQuery] = useState('')
17
24
  const typed = query.trim()
25
+ const known = options.some((option) => option.value === typed || option.label === typed)
18
26
 
19
27
  return (
20
28
  <Command className="em-command" loop>
@@ -27,12 +35,18 @@ function Options({ type, options, chosen, onChoose, onClear }) {
27
35
  <Command.List className="em-list">
28
36
  <Command.Empty className="em-empty">No options</Command.Empty>
29
37
  {options.map((option) => (
30
- <Command.Item key={option} value={option} className="em-item" onSelect={() => onChoose(option)}>
31
- <span className="em-check">{chosen.includes(option) ? '✓' : ''}</span>
32
- {option}
38
+ <Command.Item
39
+ key={option.value}
40
+ value={option.value}
41
+ keywords={[option.label]}
42
+ className="em-item"
43
+ onSelect={() => onChoose(option.value)}
44
+ >
45
+ <span className="em-check">{chosen.includes(option.value) ? '✓' : ''}</span>
46
+ {option.label}
33
47
  </Command.Item>
34
48
  ))}
35
- {type === 'text' && typed && !options.includes(typed) && (
49
+ {type === 'text' && typed && !known && (
36
50
  <Command.Item value={typed} className="em-item" onSelect={() => (setQuery(''), onChoose(typed))}>
37
51
  <span className="em-check">+</span>
38
52
  Add “{typed}”
@@ -49,37 +63,85 @@ function Options({ type, options, chosen, onChoose, onClear }) {
49
63
  )
50
64
  }
51
65
 
52
- export default function Editor({ fqn, name, type, values: initial, options: listed }) {
66
+ function Links({ links, onChange, onSave }) {
67
+ const set = (index, field) => (event) =>
68
+ onChange(links.map((link, at) => (at === index ? { ...link, [field]: event.target.value } : link)))
69
+ const onKeyDown = (event) => event.key === 'Enter' && (event.preventDefault(), onSave())
70
+
71
+ return (
72
+ <div className="em-command">
73
+ {links.map((link, index) => (
74
+ <div className="em-link" key={index}>
75
+ <input
76
+ className="em-input"
77
+ type="url"
78
+ placeholder="https://…"
79
+ aria-label="URL"
80
+ value={link.url}
81
+ onChange={set(index, 'url')}
82
+ onKeyDown={onKeyDown}
83
+ autoFocus={index === links.length - 1}
84
+ />
85
+ <input
86
+ className="em-input"
87
+ placeholder="Label"
88
+ aria-label="Label"
89
+ value={link.label ?? ''}
90
+ onChange={set(index, 'label')}
91
+ onKeyDown={onKeyDown}
92
+ />
93
+ <button
94
+ type="button"
95
+ className="em-remove"
96
+ aria-label={`Remove ${link.url || 'link'}`}
97
+ onClick={() => onChange(links.filter((_, at) => at !== index))}
98
+ >
99
+ ×
100
+ </button>
101
+ </div>
102
+ ))}
103
+ <button type="button" className="em-item em-muted em-clear" onClick={() => onChange([...links, { url: '', label: '' }])}>
104
+ <span className="em-check">+</span>
105
+ Add link
106
+ </button>
107
+ </div>
108
+ )
109
+ }
110
+
111
+ export default function Editor({ fqn, name, property = false, type, values: initial, options: listed = [] }) {
53
112
  const [values, setValues] = useState(initial)
54
113
  const [open, setOpen] = useState(false)
55
114
  const [draft, setDraft] = useState(initial)
56
115
  const [error, setError] = useState('')
57
116
  const cancelled = useRef(false)
58
- const options = [...new Set([...listed, ...draft])]
117
+ const labels = new Map(listed.map((option) => [option.value, option.label]))
118
+ const labelOf = (value) => labels.get(value) ?? value
119
+ const options = [...listed, ...draft.filter((value) => !labels.has(value)).map((value) => ({ value, label: value }))]
59
120
  const listedType = ['select', 'multiselect', 'text'].includes(type)
60
121
 
61
122
  const save = (value) => {
62
123
  setOpen(false)
63
- const next = listOf(value)
64
- if (same(next, values)) return
124
+ const next = type === 'links' ? linksOf(value) : listOf(value)
125
+ if (keyOf(next) === keyOf(values)) return
65
126
  const previous = values
66
127
  setValues(next)
67
128
  setError('')
68
- post(fqn, name, value).catch((failure) => {
129
+ post({ fqn, [property ? 'property' : 'key']: name, value: type === 'links' ? next : value }).catch((failure) => {
69
130
  setValues(previous)
70
131
  setError(failure.message)
71
132
  })
72
133
  }
134
+ const saveDraft = () => save(type === 'multiselect' || type === 'links' ? draft : (draft[0] ?? '').trim())
73
135
 
74
- // A multiselect and the plain inputs keep a draft and save it on close, unless Escape closed them.
136
+ // Everything but a select and text keeps a draft and saves it on close, unless Escape closed it.
75
137
  // A multiselect can't save per toggle because every save reloads the page.
76
138
  const onOpenChange = (next) => {
77
139
  if (next) {
78
140
  cancelled.current = false
79
- setDraft(values)
141
+ setDraft(type === 'links' && !values.length ? [{ url: '', label: '' }] : values)
80
142
  return setOpen(true)
81
143
  }
82
- if (!cancelled.current && (type === 'multiselect' || !listedType)) return save(type === 'multiselect' ? draft : draft[0] ?? '')
144
+ if (!cancelled.current && (type === 'multiselect' || !listedType)) return saveDraft()
83
145
  setOpen(false)
84
146
  }
85
147
  const onChoose = (option) => {
@@ -87,7 +149,7 @@ export default function Editor({ fqn, name, type, values: initial, options: list
87
149
  setDraft(draft.includes(option) ? draft.filter((value) => value !== option) : [...draft, option])
88
150
  }
89
151
 
90
- const label = `${name}: ${values.join(', ') || 'not set'}`
152
+ const text = shown(type, values, labelOf)
91
153
  const problem = error && (
92
154
  <span className="em-error" role="alert" title={error}>
93
155
  {' '}
@@ -111,34 +173,51 @@ export default function Editor({ fqn, name, type, values: initial, options: list
111
173
  )
112
174
  }
113
175
 
176
+ const editor = listedType ? (
177
+ <Options type={type} options={options} chosen={draft} onChoose={onChoose} onClear={() => save('')} />
178
+ ) : type === 'links' ? (
179
+ <Links links={draft} onChange={setDraft} onSave={saveDraft} />
180
+ ) : type === 'textarea' ? (
181
+ <div className="em-command">
182
+ <textarea
183
+ className="em-input em-textarea"
184
+ aria-label={name}
185
+ value={draft[0] ?? ''}
186
+ onChange={(event) => setDraft([event.target.value])}
187
+ onKeyDown={(event) => event.key === 'Enter' && (event.metaKey || event.ctrlKey) && saveDraft()}
188
+ autoFocus
189
+ />
190
+ <p className="em-hint">⌘ or Ctrl + Enter or a click outside saves. Escape cancels. Markdown works.</p>
191
+ </div>
192
+ ) : (
193
+ <div className="em-command">
194
+ <input
195
+ className="em-input"
196
+ type={type === 'line' ? 'text' : type}
197
+ aria-label={name}
198
+ value={draft[0] ?? ''}
199
+ onChange={(event) => setDraft([event.target.value])}
200
+ onKeyDown={(event) => event.key === 'Enter' && (event.preventDefault(), saveDraft())}
201
+ autoFocus
202
+ />
203
+ <p className="em-hint">Enter or a click outside saves. Escape cancels.{name === 'title' ? '' : ' Empty clears it.'}</p>
204
+ </div>
205
+ )
206
+
114
207
  return (
115
208
  <>
116
209
  <Popover.Root open={open} onOpenChange={onOpenChange}>
117
- <Popover.Trigger className="em-value" aria-label={`Edit ${label}`}>
118
- {values.join(', ') || '—'}
210
+ <Popover.Trigger className={type === 'textarea' ? 'em-value em-multiline' : 'em-value'} aria-label={`Edit ${name}: ${text || 'not set'}`}>
211
+ {text || '—'}
119
212
  </Popover.Trigger>
120
213
  <Popover.Portal>
121
214
  <Popover.Content
122
- className="em-popover"
215
+ className={type === 'textarea' || type === 'links' ? 'em-popover em-wide' : 'em-popover'}
123
216
  align="start"
124
217
  sideOffset={6}
125
218
  onEscapeKeyDown={() => (cancelled.current = true)}
126
219
  >
127
- {listedType ? (
128
- <Options type={type} options={options} chosen={draft} onChoose={onChoose} onClear={() => save('')} />
129
- ) : (
130
- <div className="em-command">
131
- <input
132
- className="em-input"
133
- type={type}
134
- value={draft[0] ?? ''}
135
- onChange={(event) => setDraft([event.target.value])}
136
- onKeyDown={(event) => event.key === 'Enter' && (event.preventDefault(), save(draft[0]?.trim() ?? ''))}
137
- autoFocus
138
- />
139
- <p className="em-hint">Enter or a click outside saves. Escape cancels. Empty removes the key.</p>
140
- </div>
141
- )}
220
+ {editor}
142
221
  </Popover.Content>
143
222
  </Popover.Portal>
144
223
  </Popover.Root>
package/Metadata.astro CHANGED
@@ -9,21 +9,32 @@ interface Props {
9
9
  name: string
10
10
  }
11
11
 
12
+ type Option = string | { value: string; label?: string }
13
+
12
14
  const types = ['text', 'number', 'date', 'url', 'boolean', 'select', 'multiselect']
13
15
 
14
16
  const { of, name } = Astro.props
15
17
  const fields = existsSync(config.fieldsFile) ? JSON.parse(readFileSync(config.fieldsFile, 'utf8')) : {}
16
- const field: { type: string; options?: string[] } = fields[name] ?? { type: 'text' }
18
+ const field: { type: string; options?: Option[] } = fields[name] ?? { type: 'text' }
17
19
  if (!types.includes(field.type)) {
18
20
  throw new Error(`Unknown type "${field.type}" for metadata "${name}". Known types: ${types.join(', ')}`)
19
21
  }
20
22
 
21
23
  const valuesOf = (fqn: string) => [model.element(fqn).getMetadata(name) ?? []].flat()
22
24
  const values = valuesOf(of)
23
- const inUse = [...model.elements()].flatMap((element) => valuesOf(element.id))
24
- const options = [...new Set([...(field.options ?? []), ...inUse])]
25
+ const listed = (field.options ?? []).map((option) =>
26
+ typeof option === 'string' ? { value: option, label: option } : { value: option.value, label: option.label ?? option.value },
27
+ )
28
+ const labels = new Map(listed.map((option) => [option.value, option.label]))
29
+ const inUse = [...new Set([...model.elements()].flatMap((element) => valuesOf(element.id)))]
30
+ const options = [...listed, ...inUse.filter((value) => !labels.has(value)).map((value) => ({ value, label: value }))]
25
31
 
26
- const text = field.type === 'boolean' && values.length ? (values[0] === 'true' ? 'Yes' : 'No') : values.join(', ')
32
+ const text =
33
+ field.type === 'boolean' && values.length
34
+ ? values[0] === 'true'
35
+ ? 'Yes'
36
+ : 'No'
37
+ : values.map((value) => labels.get(value) ?? value).join(', ')
27
38
  ---
28
39
 
29
40
  {
package/Property.astro ADDED
@@ -0,0 +1,38 @@
1
+ ---
2
+ import Editor from './Editor.jsx'
3
+ import { model } from './model.js'
4
+
5
+ interface Props {
6
+ of: string
7
+ name: 'title' | 'summary' | 'description' | 'technology' | 'links'
8
+ }
9
+
10
+ const types = { title: 'line', technology: 'line', summary: 'textarea', description: 'textarea', links: 'links' }
11
+
12
+ const { of, name } = Astro.props
13
+ const type = types[name]
14
+ if (!type) throw new Error(`Unknown property "${name}". Known properties: ${Object.keys(types).join(', ')}`)
15
+
16
+ const element = model.element(of)
17
+ const rich = name === 'summary' || name === 'description' ? element[name] : null
18
+ const links = element.links.map(({ url, title }) => (title ? { url, label: title } : { url }))
19
+ const values =
20
+ name === 'links' ? links : [rich ? (rich.$source?.md ?? rich.$source?.txt ?? '') : (element[name] ?? '')].filter(Boolean)
21
+ ---
22
+
23
+ {
24
+ import.meta.env.DEV ? (
25
+ <Editor client:load fqn={of} name={name} property type={type} values={values} />
26
+ ) : name === 'links' ? (
27
+ links.map((link, index) => (
28
+ <>
29
+ {index > 0 && ', '}
30
+ <a href={link.url}>{link.label ?? link.url}</a>
31
+ </>
32
+ ))
33
+ ) : rich?.isMarkdown ? (
34
+ <Fragment set:html={rich.html} />
35
+ ) : (
36
+ values.join('')
37
+ )
38
+ }
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @architecture-model/editable-metadata
2
2
 
3
- Shows the metadata of [LikeC4](https://likec4.dev/) elements in [Astro](https://astro.build/) pages. In the dev server the values are click-to-edit and save back to the model; in the built site they are plain text.
3
+ Shows the metadata and properties of [LikeC4](https://likec4.dev/) elements in [Astro](https://astro.build/) pages. In the dev server the values are click-to-edit and save back to the model; in the built site they are plain text.
4
4
 
5
5
  Made for repositories created with `npm create @architecture-model`, and works in any Astro site that has a LikeC4 model.
6
6
 
@@ -73,6 +73,47 @@ Without configuration every key is text. To give keys other types, create `model
73
73
 
74
74
  The options of `select` and `multiselect` are the ones listed plus every value already in use in the model. A `url` is a link in the built site.
75
75
 
76
+ An option can have a label that pages and editors show in place of the stored value:
77
+
78
+ ```json
79
+ {
80
+ "owners": { "type": "multiselect", "options": [{ "value": "igor", "label": "Igor Morozov" }, "platform-team"] }
81
+ }
82
+ ```
83
+
84
+ ## Derived values
85
+
86
+ A value that follows from the model, such as whether an external system calls you back, is asked in the page instead of stored in the model. Define a function of the [element](https://likec4.dev/tooling/model-api/) and the model, and show its answer with `Derived`:
87
+
88
+ ```mdx
89
+ import Derived from '@architecture-model/editable-metadata/Derived.astro'
90
+
91
+ export const ours = (element, model) => element.id === 'shop' || element.isDescendantOf(model.element('shop'))
92
+ export const callsBack = (element, model) => [...element.outgoing()].some(({ target }) => ours(target, model))
93
+
94
+ Payments calls us back: <Derived of="payments" value={callsBack} />
95
+ ```
96
+
97
+ A function returns a string, a number, a list, or a boolean, shown as Yes or No. A derived value is never editable.
98
+
99
+ ## Properties
100
+
101
+ `Property` shows and edits an element's own `title`, `summary`, `description`, `technology` or `links`:
102
+
103
+ ```mdx
104
+ import Property from '@architecture-model/editable-metadata/Property.astro'
105
+
106
+ <Property of="system" name="description" />
107
+ ```
108
+
109
+ | Property | Editor | In the built site |
110
+ | --- | --- | --- |
111
+ | `title`, `technology` | one-line input | text |
112
+ | `summary`, `description` | text area; ⌘ or Ctrl + Enter saves, line breaks are saved as a Markdown string | Markdown rendered as HTML |
113
+ | `links` | a URL and a label per row | links |
114
+
115
+ A title can't be emptied. Links replace the element's own links, and a row without a URL is dropped.
116
+
76
117
  ## Options
77
118
 
78
119
  ```js
@@ -86,4 +127,4 @@ Both paths are relative to the Astro project root.
86
127
 
87
128
  ## How it saves
88
129
 
89
- The dev server gets an endpoint that rewrites the element's `metadata` block with [likec4-mutator](https://github.com/rebaseandpanic/likec4-mutator), leaving the rest of the file as it was. It only accepts requests from the page itself, and it does not exist in the built site.
130
+ The dev server gets an endpoint that rewrites the edited metadata key or property of the element with [likec4-mutator](https://github.com/rebaseandpanic/likec4-mutator), leaving the rest of the file as it was. It only accepts requests from the page itself, and it does not exist in the built site.
package/editor.css CHANGED
@@ -3,6 +3,9 @@
3
3
  cursor: pointer;
4
4
  border-bottom: 1px dashed currentColor;
5
5
  }
6
+ .em-multiline {
7
+ white-space: pre-line;
8
+ }
6
9
  .em-value:focus-visible {
7
10
  outline: 2px solid var(--sl-color-accent, Highlight);
8
11
  outline-offset: 2px;
@@ -28,6 +31,35 @@
28
31
  line-height: 1.4;
29
32
  }
30
33
 
34
+ .em-wide {
35
+ width: min(32rem, calc(100vw - 2rem));
36
+ max-width: none;
37
+ }
38
+
39
+ .em-textarea {
40
+ min-height: 8rem;
41
+ resize: vertical;
42
+ }
43
+
44
+ .em-link {
45
+ display: grid;
46
+ grid-template-columns: 3fr 2fr auto;
47
+ align-items: center;
48
+ }
49
+ .em-link .em-input {
50
+ min-width: 0;
51
+ }
52
+ .em-remove {
53
+ all: unset;
54
+ padding: 0 0.5rem;
55
+ cursor: pointer;
56
+ color: var(--em-muted);
57
+ }
58
+ .em-remove:hover,
59
+ .em-remove:focus-visible {
60
+ color: inherit;
61
+ }
62
+
31
63
  .em-command {
32
64
  display: flex;
33
65
  flex-direction: column;
package/editor.js CHANGED
@@ -1,6 +1,20 @@
1
+ import { randomUUID } from 'node:crypto'
1
2
  import { globSync, readFileSync, writeFileSync } from 'node:fs'
2
3
  import { join } from 'node:path'
3
4
 
5
+ export const properties = ['title', 'summary', 'description', 'technology', 'links']
6
+
7
+ const update = async (dir, fqn, patch, rewrite = (text) => text) => {
8
+ const sources = Object.fromEntries(
9
+ globSync('**/*.c4', { cwd: dir }).map((file) => [file, readFileSync(join(dir, file), 'utf8')]),
10
+ )
11
+ const { LikeC4Mutator } = await import('likec4-mutator')
12
+ const mutator = LikeC4Mutator.fromFiles(sources)
13
+ const { changedFiles } = mutator.updateElement(fqn, patch)
14
+ const updated = mutator.serialize()
15
+ for (const file of changedFiles) writeFileSync(join(dir, file), rewrite(updated[file]))
16
+ }
17
+
4
18
  export async function setMetadata(dir, { fqn, key, value }) {
5
19
  const values = [value].flat().map((item) => (typeof item === 'string' ? item.trim() : item))
6
20
  if (typeof fqn !== 'string' || !/^[A-Za-z_][\w-]*$/.test(key) || values.some((item) => typeof item !== 'string')) {
@@ -8,15 +22,31 @@ export async function setMetadata(dir, { fqn, key, value }) {
8
22
  }
9
23
  const present = values.filter(Boolean)
10
24
  const stored = present.length ? (Array.isArray(value) ? present : present[0]) : null
25
+ await update(dir, fqn, { metadata: { [key]: stored } })
26
+ }
11
27
 
12
- const sources = Object.fromEntries(
13
- globSync('**/*.c4', { cwd: dir }).map((file) => [file, readFileSync(join(dir, file), 'utf8')]),
14
- )
15
- const { LikeC4Mutator } = await import('likec4-mutator')
16
- const mutator = LikeC4Mutator.fromFiles(sources)
17
- const { changedFiles } = mutator.updateElement(fqn, { metadata: { [key]: stored } })
18
- const updated = mutator.serialize()
19
- for (const file of changedFiles) writeFileSync(join(dir, file), updated[file])
28
+ // likec4-mutator writes a summary or description on one line, so a multi-line one goes in as a token and is swapped for a Markdown string.
29
+ const asMarkdown = (token, value) => (text) =>
30
+ text.replace(new RegExp(`^([ \\t]*)(.*)'${token}'`, 'm'), (_, indent, before) => {
31
+ const lines = value.split(/\r?\n/).map((line) => line && `${indent} ${line.replace(/\\/g, '\\\\').replace(/'(?=')/g, "\\'")}`)
32
+ return `${indent}${before}'''\n${lines.join('\n')}\n${indent}'''`
33
+ })
34
+
35
+ export async function setProperty(dir, { fqn, property, value }) {
36
+ if (typeof fqn !== 'string' || !properties.includes(property)) {
37
+ throw new Error(`Expected { fqn, property, value } with one of ${properties.join(', ')} as the property`)
38
+ }
39
+ if (property === 'links') {
40
+ const valid = Array.isArray(value) && value.every((link) => /^\S+$/.test(link?.url) && ['string', 'undefined'].includes(typeof link.label))
41
+ if (!valid) throw new Error('Expected links as a list of { url, label } with no spaces in the url')
42
+ return update(dir, fqn, { links: value.map(({ url, label }) => (label?.trim() ? { url, label: label.trim() } : { url })) })
43
+ }
44
+ if (typeof value !== 'string') throw new Error(`Expected ${property} as a string`)
45
+ const text = value.trim()
46
+ if (property === 'title' && !text) throw new Error('An element needs a title')
47
+ if (!/\n/.test(text) || ['title', 'technology'].includes(property)) return update(dir, fqn, { [property]: text })
48
+ const token = `editable${randomUUID().replaceAll('-', '')}`
49
+ await update(dir, fqn, { [property]: token }, asMarkdown(token, text))
20
50
  }
21
51
 
22
52
  export const editor = ({ modelDir, fieldsFile }) => ({
@@ -31,7 +61,7 @@ export const editor = ({ modelDir, fieldsFile }) => ({
31
61
  server.ws.send({ type: 'full-reload' })
32
62
  })
33
63
 
34
- server.middlewares.use('/__model/metadata', async (req, res) => {
64
+ server.middlewares.use('/__model/element', async (req, res) => {
35
65
  // The endpoint writes to disk, so a page from another site must not be able to call it.
36
66
  if (req.method !== 'POST' || req.headers['sec-fetch-site'] !== 'same-origin') {
37
67
  res.statusCode = 403
@@ -40,7 +70,8 @@ export const editor = ({ modelDir, fieldsFile }) => ({
40
70
  try {
41
71
  let body = ''
42
72
  for await (const chunk of req) body += chunk
43
- await setMetadata(modelDir, JSON.parse(body))
73
+ const change = JSON.parse(body)
74
+ await ('property' in change ? setProperty : setMetadata)(modelDir, change)
44
75
  res.statusCode = 204
45
76
  res.end()
46
77
  } catch (error) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@architecture-model/editable-metadata",
3
- "version": "0.2.0",
4
- "description": "Show LikeC4 element metadata in Astro pages and edit it in place in the dev server",
3
+ "version": "0.3.0",
4
+ "description": "Show LikeC4 element metadata and properties in Astro pages and edit them in place in the dev server",
5
5
  "keywords": [
6
6
  "astro-integration",
7
7
  "astro-component",
@@ -19,13 +19,17 @@
19
19
  "type": "module",
20
20
  "exports": {
21
21
  ".": "./index.js",
22
- "./Metadata.astro": "./Metadata.astro"
22
+ "./Metadata.astro": "./Metadata.astro",
23
+ "./Property.astro": "./Property.astro",
24
+ "./Derived.astro": "./Derived.astro"
23
25
  },
24
26
  "files": [
25
27
  "index.js",
26
28
  "editor.js",
27
29
  "model.js",
28
30
  "Metadata.astro",
31
+ "Property.astro",
32
+ "Derived.astro",
29
33
  "Editor.jsx",
30
34
  "NoEditor.jsx",
31
35
  "editor.css"