@oslokommune/punkt-react 13.6.16 → 13.8.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 (69) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/index.d.ts +58 -76
  3. package/dist/punkt-react.es.js +5331 -35936
  4. package/dist/punkt-react.umd.js +422 -552
  5. package/package.json +11 -15
  6. package/src/components/accordion/Accordion.test.tsx +1 -1
  7. package/src/components/accordion/Accordion.tsx +1 -1
  8. package/src/components/accordion/AccordionItem.tsx +6 -5
  9. package/src/components/alert/Alert.tsx +2 -1
  10. package/src/components/breadcrumbs/Breadcrumbs.test.tsx +16 -4
  11. package/src/components/breadcrumbs/Breadcrumbs.tsx +3 -2
  12. package/src/components/button/Button.tsx +3 -2
  13. package/src/components/checkbox/Checkbox.tsx +4 -3
  14. package/src/components/footer/Footer.tsx +6 -5
  15. package/src/components/footerSimple/FooterSimple.tsx +4 -3
  16. package/src/components/icon/Icon.test.tsx +6 -19
  17. package/src/components/index.ts +1 -2
  18. package/src/components/input/Input.tsx +4 -3
  19. package/src/components/radio/RadioButton.tsx +3 -2
  20. package/src/components/searchinput/SearchInput.tsx +3 -3
  21. package/src/components/stepper/Stepper.tsx +6 -6
  22. package/src/components/table/Table.tsx +2 -1
  23. package/src/components/table/TableBody.tsx +2 -1
  24. package/src/components/table/TableData.tsx +2 -1
  25. package/src/components/table/TableDataCell.tsx +2 -1
  26. package/src/components/table/TableHeader.tsx +2 -1
  27. package/src/components/table/TableHeaderCell.tsx +2 -1
  28. package/src/components/table/TableRow.tsx +2 -1
  29. package/src/components/tabs/TabItem.tsx +77 -0
  30. package/src/components/tabs/Tabs.test.tsx +393 -1
  31. package/src/components/tabs/Tabs.tsx +86 -65
  32. package/src/components/tag/Tag.tsx +2 -1
  33. package/src/components/preview/Preview.tsx +0 -274
  34. package/src/components/preview/PreviewCode.tsx +0 -118
  35. package/src/components/preview/PreviewPropEditor.tsx +0 -266
  36. package/src/components/preview/PreviewSpecs.tsx +0 -125
  37. package/src/components/preview/PreviewWithJson.tsx +0 -519
  38. package/src/components/preview/preview-types.ts +0 -42
  39. package/src/components/preview/preview-utils.ts +0 -226
  40. package/src/components/preview/previewJson/accordion.json +0 -84
  41. package/src/components/preview/previewJson/alert.json +0 -27
  42. package/src/components/preview/previewJson/backlink.json +0 -14
  43. package/src/components/preview/previewJson/breadcrumbs.json +0 -17
  44. package/src/components/preview/previewJson/button.json +0 -28
  45. package/src/components/preview/previewJson/card.json +0 -41
  46. package/src/components/preview/previewJson/checkbox.json +0 -21
  47. package/src/components/preview/previewJson/combobox.json +0 -24
  48. package/src/components/preview/previewJson/consent.json +0 -14
  49. package/src/components/preview/previewJson/datepicker.json +0 -14
  50. package/src/components/preview/previewJson/footer-simple.json +0 -17
  51. package/src/components/preview/previewJson/footer.json +0 -29
  52. package/src/components/preview/previewJson/header.json +0 -34
  53. package/src/components/preview/previewJson/icon.json +0 -13
  54. package/src/components/preview/previewJson/input-wrapper.json +0 -39
  55. package/src/components/preview/previewJson/link.json +0 -28
  56. package/src/components/preview/previewJson/linkcard.json +0 -30
  57. package/src/components/preview/previewJson/loader.json +0 -28
  58. package/src/components/preview/previewJson/messagebox.json +0 -28
  59. package/src/components/preview/previewJson/modal.json +0 -65
  60. package/src/components/preview/previewJson/progressbar.json +0 -16
  61. package/src/components/preview/previewJson/radiobutton.json +0 -21
  62. package/src/components/preview/previewJson/searchinput.json +0 -20
  63. package/src/components/preview/previewJson/select.json +0 -73
  64. package/src/components/preview/previewJson/steps.json +0 -72
  65. package/src/components/preview/previewJson/table.json +0 -130
  66. package/src/components/preview/previewJson/tabs.json +0 -33
  67. package/src/components/preview/previewJson/tag.json +0 -26
  68. package/src/components/preview/previewJson/textarea.json +0 -18
  69. package/src/components/preview/previewJson/textinput.json +0 -18
@@ -1,125 +0,0 @@
1
- 'use client'
2
-
3
- import React from 'react'
4
- import { PktTable } from '../table/Table'
5
- import { PktTableHeader } from '../table/TableHeader'
6
- import { PktTableBody } from '../table/TableBody'
7
- import { PktTableRow } from '../table/TableRow'
8
- import { PktTableHeaderCell } from '../table/TableHeaderCell'
9
- import { PktTableDataCell } from '../table/TableDataCell'
10
- import { ISpecObject } from './preview-types'
11
-
12
- export const PktPreviewSpecs: React.FC<{ specs: ISpecObject }> = ({ specs }) => {
13
- const formatType = (type: string | string[] | number | number[]) => {
14
- if (Array.isArray(type)) {
15
- return type.join(' \n')
16
- }
17
- return type
18
- }
19
-
20
- const formatValue = (value: any) => {
21
- if (typeof value === 'boolean') {
22
- return value ? 'true' : 'false'
23
- }
24
- return value
25
- }
26
-
27
- return (
28
- <>
29
- <h2>Egenskaper</h2>
30
- <PktTable compact>
31
- <PktTableHeader>
32
- <PktTableRow>
33
- <PktTableHeaderCell>Prop</PktTableHeaderCell>
34
- <PktTableHeaderCell>Navn</PktTableHeaderCell>
35
- <PktTableHeaderCell>Beskrivelse</PktTableHeaderCell>
36
- <PktTableHeaderCell>Type</PktTableHeaderCell>
37
- <PktTableHeaderCell>Standardverdi</PktTableHeaderCell>
38
- </PktTableRow>
39
- </PktTableHeader>
40
- <PktTableBody>
41
- {Object.entries(specs.props || {}).map(([key, value]) => (
42
- <PktTableRow key={key}>
43
- <PktTableDataCell dataLabel="Prop">
44
- <pre>{key}</pre>
45
- </PktTableDataCell>
46
- <PktTableDataCell dataLabel="Navn">{(!Array.isArray(value) && value.name) || key}</PktTableDataCell>
47
- {Array.isArray(value) ? (
48
- <>
49
- <PktTableDataCell dataLabel="Beskrivelse"></PktTableDataCell>
50
- <PktTableDataCell dataLabel="Type">
51
- <pre>{value.join('\n')}</pre>
52
- </PktTableDataCell>
53
- </>
54
- ) : (
55
- <>
56
- <PktTableDataCell dataLabel="Beskrivelse">
57
- <span dangerouslySetInnerHTML={{ __html: value.description || '' }}></span>
58
- </PktTableDataCell>
59
- <PktTableDataCell dataLabel="Type">
60
- <pre>{value.type && formatType(value.type)}</pre>
61
- </PktTableDataCell>
62
- <PktTableDataCell dataLabel="Standardverdi">
63
- <pre>{formatValue(value.default)}</pre>
64
- </PktTableDataCell>
65
- </>
66
- )}
67
- </PktTableRow>
68
- ))}
69
- </PktTableBody>
70
- </PktTable>
71
- {specs.events && (
72
- <>
73
- <h2>Hendelser / handlinger</h2>
74
- <PktTable compact>
75
- <PktTableHeader>
76
- <PktTableRow>
77
- <PktTableHeaderCell>Event</PktTableHeaderCell>
78
- <PktTableHeaderCell>Beskrivelse</PktTableHeaderCell>
79
- <PktTableHeaderCell>Type</PktTableHeaderCell>
80
- </PktTableRow>
81
- </PktTableHeader>
82
- <PktTableBody>
83
- {Object.entries(specs.events || {}).map(([key, value]) => (
84
- <PktTableRow key={key}>
85
- <PktTableDataCell dataLabel="Event">
86
- <pre>{key}</pre>
87
- </PktTableDataCell>
88
- <PktTableDataCell dataLabel="Beskrivelse">
89
- <span dangerouslySetInnerHTML={{ __html: value.description || '' }}></span>
90
- </PktTableDataCell>
91
- <PktTableDataCell dataLabel="Type">{value.type}</PktTableDataCell>
92
- </PktTableRow>
93
- ))}
94
- </PktTableBody>
95
- </PktTable>
96
- </>
97
- )}
98
- {specs.slots && (
99
- <>
100
- <h2>Innhold</h2>
101
- <PktTable compact>
102
- <PktTableHeader>
103
- <PktTableRow>
104
- <PktTableHeaderCell>Slot</PktTableHeaderCell>
105
- <PktTableHeaderCell>Beskrivelse</PktTableHeaderCell>
106
- </PktTableRow>
107
- </PktTableHeader>
108
- <PktTableBody>
109
- {Object.entries(specs.slots || {}).map(([key, value]) => (
110
- <PktTableRow key={key}>
111
- <PktTableDataCell dataLabel="Slot">
112
- <pre>{key}</pre>
113
- </PktTableDataCell>
114
- <PktTableDataCell dataLabel="Beskrivelse">
115
- <span dangerouslySetInnerHTML={{ __html: value.description || '' }}></span>
116
- </PktTableDataCell>
117
- </PktTableRow>
118
- ))}
119
- </PktTableBody>
120
- </PktTable>
121
- </>
122
- )}
123
- </>
124
- )
125
- }
@@ -1,519 +0,0 @@
1
- 'use client'
2
-
3
- import React, { useState, useCallback, useRef, useEffect } from 'react'
4
-
5
- import { PktAccordion } from '../accordion/Accordion'
6
- import { PktAccordionItem } from '../accordion/AccordionItem'
7
- import { PktAlert } from '../alert/Alert'
8
- import { PktBackLink } from '../backlink/BackLink'
9
- import { PktBreadcrumbs } from '../breadcrumbs/Breadcrumbs'
10
- import { PktButton } from '../button/Button'
11
- import { PktCard } from '../card/Card'
12
- import { PktCheckbox } from '../checkbox/Checkbox'
13
- import { PktCombobox } from '../combobox/Combobox'
14
- import { PktConsent } from '../consent/Consent'
15
- import { PktDatepicker } from '../datepicker/Datepicker'
16
- import { PktFooter } from '../footer/Footer'
17
- import { PktFooterSimple } from '../footerSimple/FooterSimple'
18
- import { PktHeader } from '../header/Header'
19
- import { PktHeading } from '../heading/Heading'
20
- import { PktHelptext } from '../helptext/Helptext'
21
- import { PktIcon } from '../icon/Icon'
22
- import { PktInputWrapper } from '../inputwrapper/InputWrapper'
23
- import { PktLink } from '../link/Link'
24
- import { PktLinkCard } from '../linkcard/LinkCard'
25
- import { PktLoader } from '../loader/Loader'
26
- import { PktMessagebox } from '../messagebox/Messagebox'
27
- import { PktModal } from '../modal/Modal'
28
- import { PktProgressbar } from '../progressbar/Progressbar'
29
- import { PktRadioButton } from '../radio/RadioButton'
30
- import { PktSearchInput } from '../searchinput/SearchInput'
31
- import { PktSelect } from '../select/Select'
32
- import { PktStep } from '../stepper/Step'
33
- import { PktStepper } from '../stepper/Stepper'
34
- import { PktTable } from '../table/Table'
35
- import { PktTableBody } from '../table/TableBody'
36
- import { PktTableDataCell } from '../table/TableDataCell'
37
- import { PktTableHeader } from '../table/TableHeader'
38
- import { PktTableHeaderCell } from '../table/TableHeaderCell'
39
- import { PktTableRow } from '../table/TableRow'
40
- import { PktTabs } from '../tabs/Tabs'
41
- import { PktTag } from '../tag/Tag'
42
- import { PktTextarea } from '../textarea/Textarea'
43
- import { PktTextinput } from '../textinput/Textinput'
44
-
45
- import type { ISpecObject, TPropObject } from './preview-types'
46
- import {
47
- attachSpecStrings,
48
- getAllowedChildren,
49
- getSpecKeyForNode,
50
- normalizeChildren,
51
- updateTree,
52
- } from './preview-utils'
53
- import { PktPreviewPropEditor } from './PreviewPropEditor'
54
- import { PktPreviewCode } from './PreviewCode'
55
-
56
- const componentMap: Record<string, React.ElementType> = {
57
- PktAccordion,
58
- PktAccordionItem,
59
- PktAlert,
60
- PktBackLink,
61
- PktBreadcrumbs,
62
- PktButton,
63
- PktCard,
64
- PktCheckbox,
65
- PktCombobox,
66
- PktConsent,
67
- PktDatepicker,
68
- PktFooter,
69
- PktFooterSimple,
70
- PktHeader,
71
- PktHeading,
72
- PktHelptext,
73
- PktIcon,
74
- PktInputWrapper,
75
- PktLink,
76
- PktLinkCard,
77
- PktLoader,
78
- PktMessagebox,
79
- PktModal,
80
- PktProgressbar,
81
- PktRadioButton,
82
- PktSearchInput,
83
- PktSelect,
84
- PktStep,
85
- PktStepper,
86
- PktTable,
87
- PktTableBody,
88
- PktTableDataCell,
89
- PktTableHeader,
90
- PktTableHeaderCell,
91
- PktTableRow,
92
- PktTabs,
93
- PktTag,
94
- PktTextarea,
95
- PktTextinput,
96
- }
97
-
98
- interface PreviewProps {
99
- specs: Record<string, ISpecObject>
100
- previewJson: any
101
- fullWidth?: boolean
102
- }
103
-
104
- const CATEGORIES = {
105
- contents: 'Innhold',
106
- ui: 'Utseende',
107
- functionality: 'Funksjonalitet',
108
- accessibility: 'Tilgjengelighet',
109
- tech: 'Teknisk',
110
- other: 'Innstillinger',
111
- }
112
-
113
- function renderFromSpec(node: any, key: string | null = null, renderStringWithSpan: boolean = true): React.ReactNode {
114
- if (typeof node === 'string') return node
115
- if (node?.type === 'text') {
116
- // Render the text node's children as plain text or HTML
117
- return renderStringWithSpan ? (
118
- <span dangerouslySetInnerHTML={{ __html: node.children ?? '' }} key={key} />
119
- ) : (
120
- node.children
121
- )
122
- }
123
- const { type, props = {}, children } = node
124
- const Comp = componentMap[type] || type
125
- if (!Comp) {
126
- return null // Component not found
127
- }
128
-
129
- let renderedChildren: React.ReactNode = null
130
- if (Array.isArray(children)) {
131
- renderedChildren = children.map((child, i) => renderFromSpec(child, child.props?.id || i, type !== 'option'))
132
- } else if (children) {
133
- renderedChildren = renderFromSpec(children, children.props?.id || 0, type !== 'option')
134
- }
135
- return (
136
- <Comp
137
- key={key}
138
- {...Object.fromEntries(
139
- Object.entries(props).map(([k, v]) =>
140
- typeof v === 'object' && v !== null && 'displayAsFalse' in v && 'value' in v
141
- ? [k, (v as { value: any }).value]
142
- : [k, v],
143
- ),
144
- )}
145
- >
146
- {typeof renderedChildren === 'string' && renderStringWithSpan ? (
147
- <span dangerouslySetInnerHTML={{ __html: renderedChildren }} />
148
- ) : (
149
- renderedChildren
150
- )}
151
- </Comp>
152
- )
153
- }
154
-
155
- // Recursive prop editor
156
- const RecursivePropEditor = React.memo(function RecursivePropEditor({
157
- node,
158
- path = [],
159
- previewSpec,
160
- specs,
161
- onChange,
162
- onAddChild,
163
- onRemoveChild,
164
- }: {
165
- node: any
166
- previewSpec: any
167
- path?: (string | number)[]
168
- specs: Record<string, ISpecObject>
169
- onChange: (path: (string | number)[], newNode: any) => void
170
- onAddChild: (path: (string | number)[], newChild: any) => void
171
- onRemoveChild: (path: (string | number)[], index: number) => void
172
- }) {
173
- if (!node || typeof node !== 'object' || !node.type) {
174
- console.warn('RecursivePropEditor: node is not a valid preview node', { node, path, previewSpec })
175
- return null
176
- }
177
- if (node?.type === 'text') {
178
- return (
179
- <PktPreviewPropEditor
180
- propKey="text"
181
- props={{ text: typeof node === 'string' ? node : node.children || '' }}
182
- spec={{ type: 'longstring', label: 'Tekst' }}
183
- handleChange={(_, v) => {
184
- onChange([...path, 'children'], v)
185
- }}
186
- />
187
- )
188
- }
189
- const allowedChildren = Array.isArray(previewSpec.children) ? previewSpec.children : []
190
- const { props = {}, children: rawChildren = [], type } = node
191
- const children = normalizeChildren(rawChildren)
192
- const specKey = node.spec || getSpecKeyForNode(node, previewSpec)
193
- const spec = specs[specKey as string]
194
- if (!spec) {
195
- console.warn(`No spec found for type "${type}" with key "${specKey}"`, { node, props, specs, previewSpec })
196
- return null // No spec found for this type
197
- }
198
- const propKeys = spec.props ? Object.keys(spec.props) : Object.keys(props)
199
- if (propKeys.length === 0 && children.length === 0) {
200
- console.warn(`No props or children defined for type "${type}" with spec key "${specKey}"`)
201
- return null // No props or children to edit
202
- }
203
- function applyDefaults(defaults: any, n: number) {
204
- const result: any = {}
205
- for (const key in defaults) {
206
- const val = defaults[key]
207
- if (typeof val === 'string') {
208
- result[key] = val.replace(/\$n/g, String(n + 1))
209
- } else {
210
- result[key] = val
211
- }
212
- }
213
- return result
214
- }
215
-
216
- // Group prop keys by category
217
- const categoryOrder = Object.keys(CATEGORIES)
218
- const groupedProps: Record<string, string[]> = {}
219
-
220
- propKeys.forEach((key) => {
221
- const cat = (spec.props?.[key] as TPropObject)?.category || 'other'
222
- if (!groupedProps[cat]) groupedProps[cat] = []
223
- groupedProps[cat].push(key)
224
- })
225
-
226
- // Sort categories by CATEGORIES order
227
- let sortedCategories = categoryOrder
228
- .filter((cat) => groupedProps[cat]?.length)
229
- .concat(Object.keys(groupedProps).filter((cat) => !categoryOrder.includes(cat)))
230
-
231
- // Find allowed text child definition (if any)
232
- const allowedTextChild = allowedChildren.find((c: any) => c.type === 'text')
233
- // Find existing text child (if any)
234
- const textChildIndex = children.findIndex((c) => c.type === 'text')
235
- const textChild = textChildIndex !== -1 ? children[textChildIndex] : null
236
-
237
- // Ensure 'contents' is present if there's a textChild
238
- if (textChild && !sortedCategories.includes('contents')) {
239
- sortedCategories = ['contents', ...sortedCategories]
240
- if (!groupedProps['contents']) groupedProps['contents'] = []
241
- }
242
-
243
- return (
244
- <>
245
- <PktHeading level={2} size="xsmall">
246
- {type}
247
- </PktHeading>
248
- <PktAccordion compact skin="outlined">
249
- {sortedCategories.map((cat, index) => (
250
- <PktAccordionItem
251
- key={cat}
252
- title={CATEGORIES[cat as keyof typeof CATEGORIES] || cat}
253
- id={`cat-${cat}`}
254
- isOpen={index === 0}
255
- >
256
- <div className="pkt-preview__options">
257
- {groupedProps[cat].map((key) => {
258
- const propSpec = (spec.props?.[key] || {}) as TPropObject
259
- // Check for showIf condition
260
- if (propSpec.showIf) {
261
- // All conditions in showIf must be met
262
- const shouldShow = Object.entries(propSpec.showIf).every(([depKey, depValue]) => {
263
- if (Array.isArray(depValue)) {
264
- return depValue.includes(props[depKey])
265
- }
266
- return props[depKey] === depValue
267
- })
268
- if (!shouldShow) return null
269
- }
270
- return (
271
- <PktPreviewPropEditor
272
- key={key}
273
- propKey={key}
274
- props={props}
275
- spec={propSpec}
276
- handleChange={(k, v) => {
277
- const valueToStore =
278
- propSpec.displayAsFalse && typeof v === 'boolean' ? { value: v, displayAsFalse: true } : v
279
- onChange([...path, 'props', key], valueToStore)
280
- }}
281
- />
282
- )
283
- })}
284
- {/* If this is the contents category, and text child is allowed, render its editor here */}
285
- {cat === 'contents' && allowedTextChild && textChild && (
286
- <PktPreviewPropEditor
287
- propKey="text"
288
- props={{ text: typeof textChild === 'string' ? textChild : textChild.children || '' }}
289
- spec={{ type: 'longstring', name: allowedTextChild.name || 'Tekst' }}
290
- handleChange={(_, v) => {
291
- onChange([...path, 'children', textChildIndex, 'children'], v)
292
- }}
293
- />
294
- )}
295
- </div>
296
- </PktAccordionItem>
297
- ))}
298
- </PktAccordion>
299
- {Array.isArray(children) && children.length > 0 && (
300
- <>
301
- {children.some((child) => child.type !== 'text') && <h2 className="pkt-inputwrapper__label">Innhold</h2>}
302
- <PktAccordion compact skin="outlined">
303
- {children.map((child, i) => {
304
- const allowedChildren = getAllowedChildren(previewSpec)
305
- const childPreviewSpec = allowedChildren.find((c) => c.type === child.type) || previewSpec
306
- // text child as string
307
- if (child.type === 'text') {
308
- return
309
- }
310
-
311
- // non-text children
312
- return (
313
- <PktAccordionItem
314
- key={child.props?.id || `child-${i}`}
315
- title={`${child.props?.id || `${i}`} (${child.type})`}
316
- id={`child-${i}`}
317
- >
318
- <div className="pkt-preview__object-editor">
319
- <RecursivePropEditor
320
- node={child}
321
- path={[...path, 'children', i]}
322
- previewSpec={childPreviewSpec}
323
- specs={specs}
324
- onChange={onChange}
325
- onAddChild={onAddChild}
326
- onRemoveChild={onRemoveChild}
327
- />
328
- <div className="pkt-preview__remove-object">
329
- <PktButton
330
- onClick={() => {
331
- confirm(`Er du sikker på at du vil fjerne ${child.type} ${child.props?.id || `#${i + 1}`}`) &&
332
- onRemoveChild([...path, 'children'], i)
333
- }}
334
- style={{ marginTop: 8 }}
335
- iconName="trash-can"
336
- skin="tertiary"
337
- size="small"
338
- variant="icon-only"
339
- >
340
- Fjern {child.type}
341
- </PktButton>
342
- </div>
343
- </div>
344
- </PktAccordionItem>
345
- )
346
- })}
347
- </PktAccordion>
348
- </>
349
- )}
350
- {allowedChildren.length > 0 &&
351
- allowedChildren
352
- .filter((childDef: any) => childDef.type !== 'text')
353
- .map((childDef: any, i: number) => (
354
- <PktButton
355
- key={childDef.type}
356
- skin="primary"
357
- size="small"
358
- variant="icon-left"
359
- iconName="plus-circle-fill"
360
- style={{ width: '100%', marginTop: 8 }}
361
- onClick={() => {
362
- const n = Array.isArray(children) ? children.length : 0
363
- const newChild = {
364
- type: childDef.type,
365
- props: applyDefaults(childDef.defaults || {}, n),
366
- children: childDef.slot || '',
367
- id: `child-${Date.now()}`,
368
- }
369
- onAddChild(path, newChild)
370
- }}
371
- >
372
- Legg til {childDef.name || childDef.type}
373
- </PktButton>
374
- ))}
375
- </>
376
- )
377
- })
378
-
379
- export const PktPreviewWithJson: React.FC<PreviewProps> = ({ specs, previewJson, fullWidth }) => {
380
- const [tree, setTree] = useState(attachSpecStrings(previewJson.preview, previewJson))
381
- const [mode, setMode] = useState<'light' | 'dark'>('light')
382
- const [previewHtml, setPreviewHtml] = useState('')
383
- const [previewKey, setPreviewKey] = useState(0)
384
- const panelRef = useRef<HTMLDialogElement>(null)
385
- const previewComponent = useRef<HTMLDivElement>(null)
386
-
387
- useEffect(() => {
388
- setPreviewHtml(previewComponent.current?.innerHTML ?? '')
389
- if (previewJson.rerender) {
390
- setPreviewKey((k) => k + 1)
391
- }
392
- }, [tree])
393
-
394
- const handleEditorChange = useCallback(
395
- (path: (string | number)[], value: any) => {
396
- setTree(attachSpecStrings(JSON.parse(JSON.stringify(updateTree(tree, path, value))), previewJson))
397
- },
398
- [tree],
399
- )
400
-
401
- // Add a child to a node at a given path
402
- const handleAddChild = useCallback((path: (string | number)[], newChild: any) => {
403
- setTree((prevTree: any) => {
404
- const parent = path.length === 0 ? prevTree : path.reduce((acc, key) => acc[key], prevTree)
405
- const updatedChildren = Array.isArray(parent.children) ? [...parent.children, newChild] : [newChild]
406
- return attachSpecStrings(
407
- JSON.parse(JSON.stringify(updateTree(prevTree, [...path, 'children'], updatedChildren))),
408
- previewJson,
409
- )
410
- })
411
- }, [])
412
-
413
- // Remove a child at a given path (path points to the children array, index is the child to remove)
414
- const handleRemoveChild = useCallback((path: (string | number)[], index: number) => {
415
- setTree((prevTree: any) => {
416
- const parent = path.length === 0 ? prevTree : path.reduce((acc, key) => acc[key], prevTree)
417
- if (!Array.isArray(parent)) return prevTree
418
- const updatedChildren = parent.filter((_: any, i: number) => i !== index)
419
- return attachSpecStrings(
420
- JSON.parse(JSON.stringify(updateTree(prevTree, [...path], updatedChildren))),
421
- previewJson,
422
- )
423
- })
424
- }, [])
425
-
426
- return (
427
- <div className="pkt-preview">
428
- <div className="pkt-preview__component-container">
429
- <div
430
- className={`pkt-preview__component ${fullWidth ? 'pkt-preview__component--fullwidth' : ''}`}
431
- data-mode={mode}
432
- >
433
- <div key={previewJson.rerender ? previewKey : undefined} ref={previewComponent}>
434
- {renderFromSpec(tree)}
435
- </div>
436
- </div>
437
- <div className="pkt-preview__panel" style={{ minWidth: 320, maxWidth: 400 }}>
438
- <div className="pkt-preview__panel__options">
439
- {previewJson.previewProps.map((key: string) => (
440
- <PktPreviewPropEditor
441
- key={key}
442
- propKey={key}
443
- hideKey
444
- props={tree.props}
445
- spec={specs[previewJson.spec]?.props?.[key] || {}}
446
- handleChange={(k, v) => {
447
- // If displayAsFalse is set in the spec, always store as object
448
- const spec = specs[previewJson.spec]?.props?.[key] || {}
449
- const valueToStore =
450
- (spec as TPropObject).displayAsFalse && typeof v === 'boolean'
451
- ? { value: v, displayAsFalse: true }
452
- : v
453
- setTree(
454
- attachSpecStrings(
455
- JSON.parse(JSON.stringify(updateTree(tree, ['props', key], valueToStore))),
456
- previewJson,
457
- ),
458
- )
459
- }}
460
- />
461
- ))}
462
- {specs[previewJson.spec]['dark-mode'] && (
463
- <div className="pkt-preview__mode">
464
- <PktCheckbox
465
- id="mode"
466
- label="Mørk modus"
467
- type="checkbox"
468
- checked={mode === 'dark'}
469
- onChange={(e) => setMode(e.target.checked ? 'dark' : 'light')}
470
- labelPosition="right"
471
- isSwitch
472
- />
473
- </div>
474
- )}
475
- </div>
476
- <div className="pkt-preview__panel__button">
477
- <PktButton
478
- skin="secondary"
479
- variant="icon-right"
480
- iconName="crane"
481
- className="pkt-preview__open-panel"
482
- onClick={() => {
483
- panelRef.current && panelRef.current.showModal()
484
- }}
485
- >
486
- Åpne kodebygger
487
- </PktButton>
488
- <PktModal
489
- variant="drawer"
490
- drawerPosition="right"
491
- size="small"
492
- ref={panelRef}
493
- transparentBackdrop
494
- closeOnBackdropClick
495
- >
496
- <div className="pkt-preview__options">
497
- <PktHeading level={1} size="medium">
498
- Kodebygger
499
- </PktHeading>
500
-
501
- <RecursivePropEditor
502
- node={tree}
503
- previewSpec={previewJson}
504
- specs={specs}
505
- onChange={handleEditorChange}
506
- onAddChild={handleAddChild}
507
- onRemoveChild={handleRemoveChild}
508
- />
509
- </div>
510
- </PktModal>
511
- </div>
512
- </div>
513
- </div>
514
- <PktPreviewCode tree={tree} specs={specs} html={previewHtml} />
515
- </div>
516
- )
517
- }
518
-
519
- PktPreviewWithJson.displayName = 'PktPreviewWithJson'
@@ -1,42 +0,0 @@
1
- export interface ISpecObject {
2
- name?: string
3
- 'css-class'?: string
4
- 'dark-mode'?: boolean
5
- isElement?: boolean
6
- props?: Record<string, TPropObject | string[] | number[]>
7
- events?: Record<string, TPropObject>
8
- slots?: Record<string, any>
9
- default?: any
10
- }
11
-
12
- export interface IPropEditorProps {
13
- propKey: string
14
- spec: any
15
- props: any
16
- hideKey?: boolean
17
- handleChange: (key: string, value: any, displayAsFalse?: boolean) => void
18
- }
19
-
20
- export type TPropObject = {
21
- name?: string
22
- description?: string
23
- type?: string | string[] | number | number[]
24
- variant?: string
25
- converter?: string
26
- reflect?: boolean
27
- default?: any
28
- previewDefault?: any
29
- items?: TPropObject
30
- displayAsFalse?: boolean
31
- category?: string
32
- showIf?: Record<string, any>
33
- }
34
-
35
- export type TPreviewTreeNode = {
36
- type: string
37
- spec?: string
38
- props?: Record<string, any>
39
- children?: any
40
- }
41
-
42
- export type TSpecs = Record<string, ISpecObject>