panelui-native 0.46.0 → 0.49.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 (72) hide show
  1. package/README.md +5 -1
  2. package/lib/module/components/accordion/index.js +32 -4
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/button/index.js +83 -15
  5. package/lib/module/components/button/index.js.map +1 -1
  6. package/lib/module/components/button-group/index.js +186 -0
  7. package/lib/module/components/button-group/index.js.map +1 -0
  8. package/lib/module/components/color-picker/index.js +110 -1
  9. package/lib/module/components/color-picker/index.js.map +1 -1
  10. package/lib/module/components/date-time-picker/index.js +23 -5
  11. package/lib/module/components/date-time-picker/index.js.map +1 -1
  12. package/lib/module/components/fab/index.js +514 -0
  13. package/lib/module/components/fab/index.js.map +1 -0
  14. package/lib/module/components/markdown-editor/index.js +406 -0
  15. package/lib/module/components/markdown-editor/index.js.map +1 -0
  16. package/lib/module/components/markdown-editor/markdown-transforms.js +243 -0
  17. package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -0
  18. package/lib/module/components/questionnaire/index.js +1312 -0
  19. package/lib/module/components/questionnaire/index.js.map +1 -0
  20. package/lib/module/components/tabs/index.js +94 -18
  21. package/lib/module/components/tabs/index.js.map +1 -1
  22. package/lib/module/components/time-picker/index.js +34 -6
  23. package/lib/module/components/time-picker/index.js.map +1 -1
  24. package/lib/module/components/tree/index.js +500 -0
  25. package/lib/module/components/tree/index.js.map +1 -0
  26. package/lib/module/icons/index.js +217 -0
  27. package/lib/module/icons/index.js.map +1 -1
  28. package/lib/module/index.js +6 -1
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/typescript/src/components/accordion/index.d.ts +21 -0
  31. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  32. package/lib/typescript/src/components/button/index.d.ts +21 -0
  33. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  34. package/lib/typescript/src/components/button-group/index.d.ts +212 -0
  35. package/lib/typescript/src/components/button-group/index.d.ts.map +1 -0
  36. package/lib/typescript/src/components/color-picker/index.d.ts +82 -1
  37. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
  38. package/lib/typescript/src/components/date-time-picker/index.d.ts +7 -1
  39. package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -1
  40. package/lib/typescript/src/components/fab/index.d.ts +285 -0
  41. package/lib/typescript/src/components/fab/index.d.ts.map +1 -0
  42. package/lib/typescript/src/components/markdown-editor/index.d.ts +102 -0
  43. package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -0
  44. package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +76 -0
  45. package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts.map +1 -0
  46. package/lib/typescript/src/components/questionnaire/index.d.ts +336 -0
  47. package/lib/typescript/src/components/questionnaire/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/tabs/index.d.ts +9 -0
  49. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  50. package/lib/typescript/src/components/time-picker/index.d.ts +19 -1
  51. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  52. package/lib/typescript/src/components/tree/index.d.ts +125 -0
  53. package/lib/typescript/src/components/tree/index.d.ts.map +1 -0
  54. package/lib/typescript/src/icons/index.d.ts +22 -0
  55. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  56. package/lib/typescript/src/index.d.ts +8 -3
  57. package/lib/typescript/src/index.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/components/accordion/index.tsx +48 -6
  60. package/src/components/button/index.tsx +97 -15
  61. package/src/components/button-group/index.tsx +199 -0
  62. package/src/components/color-picker/index.tsx +140 -3
  63. package/src/components/date-time-picker/index.tsx +39 -3
  64. package/src/components/fab/index.tsx +583 -0
  65. package/src/components/markdown-editor/index.tsx +526 -0
  66. package/src/components/markdown-editor/markdown-transforms.ts +228 -0
  67. package/src/components/questionnaire/index.tsx +1615 -0
  68. package/src/components/tabs/index.tsx +100 -28
  69. package/src/components/time-picker/index.tsx +42 -6
  70. package/src/components/tree/index.tsx +564 -0
  71. package/src/icons/index.tsx +154 -0
  72. package/src/index.ts +77 -1
@@ -0,0 +1,228 @@
1
+ /**
2
+ * The edits a formatting toolbar makes, as functions of the text and where the
3
+ * caret is in it.
4
+ *
5
+ * Pure, and deliberately so. Every one of these takes `(text, selection)` and
6
+ * returns the text that should replace it together with where the selection
7
+ * should end up — no component, no ref, no input. What makes a formatting
8
+ * toolbar feel broken is never the characters it inserts; it is where the caret
9
+ * lands afterwards, and that is a property of these functions rather than of
10
+ * the field they are wired to.
11
+ *
12
+ * Three rules they all keep:
13
+ *
14
+ * - **Applying twice undoes it.** A toolbar button that only ever adds is a
15
+ * button you can press once, and every press after that damages the text.
16
+ * - **A selection stays selected.** Bolding three words and then italicising
17
+ * the same three has to be two presses, not a press and a re-selection.
18
+ * - **With nothing selected, the caret lands where the writing goes** — between
19
+ * the new markers rather than after them.
20
+ */
21
+
22
+ export interface EditorSelection {
23
+ start: number;
24
+ end: number;
25
+ }
26
+
27
+ export interface EditResult {
28
+ text: string;
29
+ selection: EditorSelection;
30
+ }
31
+
32
+ /** Clamps a selection to the text it belongs to, and puts it the right way round. */
33
+ function normalise(text: string, selection: EditorSelection): EditorSelection {
34
+ const a = Math.max(0, Math.min(text.length, selection.start));
35
+ const b = Math.max(0, Math.min(text.length, selection.end));
36
+ return { start: Math.min(a, b), end: Math.max(a, b) };
37
+ }
38
+
39
+ /* -------------------------------------------------------------------------- *
40
+ * Inline markers — bold, italic, inline code
41
+ * -------------------------------------------------------------------------- */
42
+
43
+ /**
44
+ * Wraps the selection in `marker`, or unwraps it if it is already wrapped.
45
+ *
46
+ * "Already wrapped" is checked both ways round: the markers may be inside the
47
+ * selection (the writer selected `**bold**`) or immediately outside it (they
48
+ * selected `bold` in the middle of `**bold**`). Both are the same intent, and a
49
+ * toolbar that only recognised one of them would add a second pair of asterisks
50
+ * to text that already had them.
51
+ */
52
+ export function toggleWrap(
53
+ text: string,
54
+ selection: EditorSelection,
55
+ marker: string
56
+ ): EditResult {
57
+ const { start, end } = normalise(text, selection);
58
+ const selected = text.slice(start, end);
59
+ const len = marker.length;
60
+
61
+ // The markers are inside the selection.
62
+ if (
63
+ selected.length >= len * 2 &&
64
+ selected.startsWith(marker) &&
65
+ selected.endsWith(marker)
66
+ ) {
67
+ const inner = selected.slice(len, selected.length - len);
68
+ return {
69
+ text: text.slice(0, start) + inner + text.slice(end),
70
+ selection: { start, end: start + inner.length },
71
+ };
72
+ }
73
+
74
+ // The markers are just outside it.
75
+ const before = text.slice(Math.max(0, start - len), start);
76
+ const after = text.slice(end, end + len);
77
+ if (before === marker && after === marker) {
78
+ return {
79
+ text: text.slice(0, start - len) + selected + text.slice(end + len),
80
+ selection: { start: start - len, end: end - len + selected.length },
81
+ };
82
+ }
83
+
84
+ const next = text.slice(0, start) + marker + selected + marker + text.slice(end);
85
+
86
+ // Nothing selected: the caret goes between the markers, which is where the
87
+ // writer is about to type. Something selected: it stays selected.
88
+ return selected
89
+ ? { text: next, selection: { start: start + len, end: end + len } }
90
+ : { text: next, selection: { start: start + len, end: start + len } };
91
+ }
92
+
93
+ /* -------------------------------------------------------------------------- *
94
+ * Line prefixes — headings, lists, quotes
95
+ * -------------------------------------------------------------------------- */
96
+
97
+ /** The span of whole lines the selection touches, even partly. */
98
+ function lineRange(text: string, selection: EditorSelection): EditorSelection {
99
+ const { start, end } = normalise(text, selection);
100
+ const from = text.lastIndexOf('\n', start - 1) + 1;
101
+ const lineEnd = text.indexOf('\n', end);
102
+ return { start: from, end: lineEnd === -1 ? text.length : lineEnd };
103
+ }
104
+
105
+ /**
106
+ * Replaces the text between two offsets and reports where the selection goes.
107
+ *
108
+ * The selection is kept over the same *lines* rather than the same character
109
+ * offsets — prefixing four lines moves everything after the first one along,
110
+ * and a selection that did not move with them would end up covering a different
111
+ * four lines than the ones just changed.
112
+ */
113
+ function replaceBlock(
114
+ text: string,
115
+ range: EditorSelection,
116
+ replacement: string
117
+ ): EditResult {
118
+ return {
119
+ text: text.slice(0, range.start) + replacement + text.slice(range.end),
120
+ selection: { start: range.start, end: range.start + replacement.length },
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Puts `prefix` on the front of every line the selection touches, or takes it
126
+ * off if every one of them already has it.
127
+ *
128
+ * Every one, not any one: a mixed block is a block the writer is trying to make
129
+ * uniform, so the useful answer there is to add rather than to remove.
130
+ */
131
+ export function toggleLinePrefix(
132
+ text: string,
133
+ selection: EditorSelection,
134
+ prefix: string
135
+ ): EditResult {
136
+ const range = lineRange(text, selection);
137
+ const lines = text.slice(range.start, range.end).split('\n');
138
+ const has = (line: string) => line.startsWith(prefix);
139
+ const next = lines.every(has)
140
+ ? lines.map((line) => line.slice(prefix.length))
141
+ : lines.map((line) => (has(line) ? line : prefix + line));
142
+
143
+ return replaceBlock(text, range, next.join('\n'));
144
+ }
145
+
146
+ /** How an ordered-list line starts, so it can be recognised and removed. */
147
+ const ORDERED = /^\d+\.\s/;
148
+
149
+ /**
150
+ * Numbers the lines the selection touches, or unnumbers them.
151
+ *
152
+ * Counted from one within the block rather than continuing whatever came above
153
+ * it: the numbers a writer types are not the numbers that get rendered — every
154
+ * markdown reader renumbers an ordered list from its first item — so the useful
155
+ * thing is for the source to read the way the output will.
156
+ */
157
+ export function toggleOrderedList(
158
+ text: string,
159
+ selection: EditorSelection
160
+ ): EditResult {
161
+ const range = lineRange(text, selection);
162
+ const lines = text.slice(range.start, range.end).split('\n');
163
+ const next = lines.every((line) => ORDERED.test(line))
164
+ ? lines.map((line) => line.replace(ORDERED, ''))
165
+ : lines.map((line, i) => `${i + 1}. ${line.replace(ORDERED, '')}`);
166
+
167
+ return replaceBlock(text, range, next.join('\n'));
168
+ }
169
+
170
+ /* -------------------------------------------------------------------------- *
171
+ * Links
172
+ * -------------------------------------------------------------------------- */
173
+
174
+ /** Placeholder left where a link's target goes. */
175
+ export const LINK_PLACEHOLDER = 'url';
176
+
177
+ /**
178
+ * Turns the selection into a link, and selects the part that still needs
179
+ * filling in.
180
+ *
181
+ * Which part that is depends on what was selected. With text selected, the
182
+ * label is written and the target is not, so the placeholder target is left
183
+ * selected and typing replaces it. With nothing selected there is neither, and
184
+ * the label comes first because it is the half the writer is thinking about.
185
+ */
186
+ export function insertLink(text: string, selection: EditorSelection): EditResult {
187
+ const { start, end } = normalise(text, selection);
188
+ const label = text.slice(start, end);
189
+ const inserted = `[${label}](${LINK_PLACEHOLDER})`;
190
+ const next = text.slice(0, start) + inserted + text.slice(end);
191
+
192
+ if (label) {
193
+ const from = start + label.length + 3; // `[` + label + `](`
194
+ return { text: next, selection: { start: from, end: from + LINK_PLACEHOLDER.length } };
195
+ }
196
+
197
+ return { text: next, selection: { start: start + 1, end: start + 1 } };
198
+ }
199
+
200
+ /* -------------------------------------------------------------------------- *
201
+ * Fenced code
202
+ * -------------------------------------------------------------------------- */
203
+
204
+ /**
205
+ * Puts the selected lines in a fence, or takes them out of one.
206
+ *
207
+ * A fence is the one construct that has to sit on lines of its own, so this
208
+ * makes room for itself: a blank line before it when it is not already at the
209
+ * start of a paragraph, and one after it at the end.
210
+ */
211
+ export function toggleFence(text: string, selection: EditorSelection): EditResult {
212
+ const range = lineRange(text, selection);
213
+ const block = text.slice(range.start, range.end);
214
+ const lines = block.split('\n');
215
+
216
+ if (lines.length >= 2 && lines[0]?.startsWith('```') && lines.at(-1) === '```') {
217
+ return replaceBlock(text, range, lines.slice(1, -1).join('\n'));
218
+ }
219
+
220
+ const leadIn = range.start > 0 && text[range.start - 1] !== '\n' ? '\n' : '';
221
+ const fenced = `${leadIn}\`\`\`\n${block}\n\`\`\``;
222
+ const result = replaceBlock(text, range, fenced);
223
+
224
+ // Inside the fence, on the line the code goes on — not around the whole
225
+ // block, which would be selecting the markers along with the content.
226
+ const inner = range.start + leadIn.length + 4;
227
+ return { text: result.text, selection: { start: inner, end: inner + block.length } };
228
+ }