@guardian/stand 0.0.0 → 0.0.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.
Files changed (64) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.prettierrc +1 -0
  4. package/.storybook/main.ts +12 -0
  5. package/.storybook/preview.tsx +83 -0
  6. package/CHANGELOG.md +7 -0
  7. package/README.md +15 -0
  8. package/dist/byline/Byline.cjs +375 -0
  9. package/dist/byline/Byline.js +273 -0
  10. package/dist/byline/Preview.cjs +52 -0
  11. package/dist/byline/Preview.js +26 -0
  12. package/dist/byline/lib.cjs +240 -0
  13. package/dist/byline/lib.js +181 -0
  14. package/dist/byline/placeholder.cjs +29 -0
  15. package/dist/byline/placeholder.js +27 -0
  16. package/dist/byline/plugins.cjs +144 -0
  17. package/dist/byline/plugins.js +123 -0
  18. package/dist/byline/schema.cjs +66 -0
  19. package/dist/byline/schema.js +59 -0
  20. package/dist/byline/styles.cjs +244 -0
  21. package/dist/byline/styles.js +234 -0
  22. package/dist/index.cjs +4 -4
  23. package/dist/index.js +1 -5
  24. package/dist/types/.storybook/main.d.ts +3 -0
  25. package/dist/types/.storybook/preview.d.ts +3 -0
  26. package/dist/types/jest-setup-after-env.d.ts +1 -0
  27. package/dist/types/src/byline/Byline.d.ts +17 -0
  28. package/dist/types/src/byline/Byline.stories.d.ts +206 -0
  29. package/dist/types/src/byline/Byline.test.d.ts +1 -0
  30. package/dist/types/src/byline/Preview.d.ts +4 -0
  31. package/dist/types/src/byline/contributors-fixture.d.ts +1 -0
  32. package/dist/types/src/byline/lib.d.ts +48 -0
  33. package/dist/types/src/byline/lib.test.d.ts +1 -0
  34. package/dist/types/src/byline/placeholder.d.ts +2 -0
  35. package/dist/types/src/byline/plugins.d.ts +4 -0
  36. package/dist/types/src/byline/schema.d.ts +2 -0
  37. package/dist/types/src/byline/styles.d.ts +11 -0
  38. package/dist/types/src/byline/theme.d.ts +44 -0
  39. package/dist/types/src/byline/util.d.ts +3 -0
  40. package/dist/types/src/index.d.ts +2 -0
  41. package/dist/types/src/mocks/prosemirror-view.d.ts +10 -0
  42. package/eslint.config.js +14 -0
  43. package/jest-setup-after-env.ts +1 -0
  44. package/jest.config.js +12 -0
  45. package/package.json +60 -129
  46. package/rollup.config.js +49 -0
  47. package/src/byline/Byline.stories.tsx +186 -0
  48. package/src/byline/Byline.test.tsx +450 -0
  49. package/src/byline/Byline.tsx +524 -0
  50. package/src/byline/Preview.tsx +59 -0
  51. package/src/byline/contributors-fixture.ts +1006 -0
  52. package/src/byline/lib.test.ts +179 -0
  53. package/src/byline/lib.ts +426 -0
  54. package/src/byline/placeholder.ts +30 -0
  55. package/src/byline/plugins.ts +186 -0
  56. package/src/byline/schema.ts +62 -0
  57. package/src/byline/styles.ts +246 -0
  58. package/src/byline/theme.ts +45 -0
  59. package/src/byline/util.ts +5 -0
  60. package/src/index.ts +2 -0
  61. package/src/mocks/prosemirror-view.ts +19 -0
  62. package/tsconfig.json +19 -0
  63. package/LICENSE +0 -201
  64. package/dist/index.d.ts +0 -3
@@ -0,0 +1,8 @@
1
+ # Changesets
2
+
3
+ Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4
+ with multi-package repos, or single-package repos to help you version and publish your code. You can
5
+ find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6
+
7
+ We have a quick list of common questions to get you started engaging with this project in
8
+ [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "public",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "ignore": []
11
+ }
package/.prettierrc ADDED
@@ -0,0 +1 @@
1
+ "@guardian/prettier"
@@ -0,0 +1,12 @@
1
+ import type { StorybookConfig } from '@storybook/react-vite';
2
+
3
+ const config: StorybookConfig = {
4
+ stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
5
+ addons: ['@storybook/addon-docs'],
6
+ framework: {
7
+ name: '@storybook/react-vite',
8
+ options: {},
9
+ },
10
+ };
11
+
12
+ export default config;
@@ -0,0 +1,83 @@
1
+ import { Global } from '@emotion/react';
2
+ import { css } from '@emotion/react';
3
+ import { withThemeFromJSXProvider } from '@storybook/addon-themes';
4
+ import type { Preview } from '@storybook/react-vite';
5
+
6
+ const ASSETS_URL = 'https://interactive.guim.co.uk/fonts/guss-webfonts/';
7
+
8
+ const Fonts = css`
9
+ @font-face {
10
+ font-family: 'Guardian Agate Sans';
11
+ src: url('${ASSETS_URL}GuardianAgateSans1Web/GuardianAgateSans1Web-Regular.woff2')
12
+ format('woff2');
13
+ font-weight: 400;
14
+ font-style: 'normal';
15
+ font-display: 'fallback';
16
+ }
17
+ @font-face {
18
+ font-family: 'Guardian Agate Sans';
19
+ src: url('${ASSETS_URL}GuardianAgateSans1Web/GuardianAgateSans1Web-Bold.woff2')
20
+ format('woff2');
21
+ font-weight: 700;
22
+ font-style: normal;
23
+ font-display: swap;
24
+ }
25
+ @font-face {
26
+ font-family: 'Guardian Agate Sans';
27
+ src: url('${ASSETS_URL}GuardianAgateSans1Web/GuardianAgateSans1Web-RegularItalic.woff2')
28
+ format('woff2');
29
+ font-weight: 400;
30
+ font-style: italic;
31
+ font-display: swap;
32
+ }
33
+ @font-face {
34
+ font-family: 'Guardian Agate Sans';
35
+ src: url('${ASSETS_URL}GuardianAgateSans1Web/GuardianAgateSans1Web-BoldItalic.woff2')
36
+ format('woff2');
37
+ font-weight: 700;
38
+ font-style: italic;
39
+ font-display: swap;
40
+ }
41
+ `;
42
+
43
+ const globalFont = {
44
+ fontFamily: '"Guardian Agate Sans", "Arial", sans-serif',
45
+ fontSize: '13px',
46
+ lineHeight: '1.2',
47
+ color: '#292929',
48
+ };
49
+
50
+ const globalStyles = css({
51
+ html: {
52
+ height: '100vh',
53
+ width: '100vw',
54
+ },
55
+ body: {
56
+ ...globalFont,
57
+ minHeight: '100vh',
58
+ width: '100vw',
59
+ margin: '0 auto',
60
+ },
61
+ });
62
+
63
+ const GlobalStyles = () => <Global styles={[Fonts, globalStyles]} />;
64
+
65
+ const preview: Preview = {
66
+ parameters: {
67
+ controls: {
68
+ matchers: {
69
+ color: /(background|color)$/i,
70
+ date: /Date$/i,
71
+ },
72
+ },
73
+ },
74
+ decorators: [
75
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- withThemeFromJSXProvider is a valid function
76
+ withThemeFromJSXProvider({
77
+ GlobalStyles, // Adds GlobalStyles to all stories
78
+ }),
79
+ ],
80
+ };
81
+
82
+ // eslint-disable-next-line import/no-default-export -- storybook expects default
83
+ export default preview;
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @guardian/stand
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - cf56688: Add changesets
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # Stand - a tools component library
2
+
3
+ _Find what you need on the (news)stand!_
4
+
5
+ Stand is component library for Guardian editorial tools. It is co-located within flexible-content as Composer is expected to be the main consumer of the UI components within Stand. But any editorial tool should be able to make use of the components as an npm package - `@guardian/stand` - and developers should feel comfortable contributing.
6
+
7
+ ## Setup
8
+
9
+ - Run `./setup.sh` in the project root (flexible-content) directory to set up pnpm, install dependencies, and build the project.
10
+
11
+ ## Tasks
12
+
13
+ - Run `pnpm install` to install dependencies.
14
+ - Run `pnpm build` to build, this makes any changes available to flexible-frontend
15
+ - Run `pnpm storybook` to run Storybook
@@ -0,0 +1,375 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('@emotion/react/jsx-runtime');
4
+ var prosemirrorInvisibles = require('@guardian/prosemirror-invisibles');
5
+ var prosemirrorDropcursor = require('prosemirror-dropcursor');
6
+ var prosemirrorHistory = require('prosemirror-history');
7
+ var prosemirrorState = require('prosemirror-state');
8
+ var prosemirrorView = require('prosemirror-view');
9
+ var react = require('react');
10
+ var lib = require('./lib.cjs');
11
+ var placeholder = require('./placeholder.cjs');
12
+ var plugins = require('./plugins.cjs');
13
+ var Preview = require('./Preview.cjs');
14
+ var schema = require('./schema.cjs');
15
+ var styles = require('./styles.cjs');
16
+
17
+ const Byline = ({
18
+ theme,
19
+ allowUntaggedContributors,
20
+ contributorLimit,
21
+ enablePreview,
22
+ placeholder: placeholder$1,
23
+ initialValue,
24
+ readOnly,
25
+ handleSave,
26
+ searchContributors,
27
+ onBlur
28
+ }) => {
29
+ const editorRef = react.useRef(null);
30
+ const viewRef = react.useRef(null);
31
+ const dropdownRef = react.useRef(null);
32
+ const [currentText, setCurrentText] = react.useState("");
33
+ const [currentOptionIndex, setCurrentOptionIndex] = react.useState(0);
34
+ const [taggedContributors, setTaggedContributors] = react.useState([]);
35
+ const [currentDoc, setCurrentDoc] = react.useState(null);
36
+ const [showDropdown, setShowDropdown] = react.useState(false);
37
+ const isTypingFromStartRange = react.useRef(
38
+ void 0
39
+ );
40
+ const trackTypingFromStart = (tr) => {
41
+ const isCursorSelection = tr.selection.from === tr.selection.to;
42
+ const currentPosition = tr.selection.from;
43
+ const hasChanges = tr.steps.length > 0;
44
+ if (!isCursorSelection) {
45
+ isTypingFromStartRange.current = void 0;
46
+ return;
47
+ }
48
+ if (!isTypingFromStartRange.current && currentPosition === 1 && hasChanges) {
49
+ isTypingFromStartRange.current = {
50
+ start: 1,
51
+ maxReached: 1,
52
+ lastPosition: 1
53
+ };
54
+ return;
55
+ }
56
+ if (isTypingFromStartRange.current) {
57
+ const { start, maxReached, lastPosition } = isTypingFromStartRange.current;
58
+ const isWithinRange = currentPosition >= start && currentPosition <= maxReached;
59
+ const isExtendingRange = hasChanges && currentPosition === maxReached + 1;
60
+ if (isWithinRange || isExtendingRange) {
61
+ if (hasChanges) {
62
+ const positionDelta = currentPosition - lastPosition;
63
+ if (positionDelta < 0) {
64
+ isTypingFromStartRange.current.maxReached = maxReached - 1;
65
+ } else if (positionDelta > 0) {
66
+ isTypingFromStartRange.current.maxReached = maxReached + 1;
67
+ }
68
+ isTypingFromStartRange.current.lastPosition = currentPosition;
69
+ }
70
+ return;
71
+ }
72
+ }
73
+ isTypingFromStartRange.current = void 0;
74
+ };
75
+ const getShouldShowDropdown = (view, selectedText) => {
76
+ if (readOnly) {
77
+ return false;
78
+ }
79
+ const showDropdownBasedOnProps = !!searchContributors || !!allowUntaggedContributors;
80
+ if (lib.hasHitContributorLimit(view.state.doc, contributorLimit)) {
81
+ return false;
82
+ }
83
+ return selectedText.trim() !== "" && showDropdownBasedOnProps;
84
+ };
85
+ const [enterHit, setEnterHit] = react.useState(false);
86
+ const checkDropdownVisibility = () => {
87
+ if (dropdownRef.current?.checkVisibility) {
88
+ return dropdownRef.current.checkVisibility();
89
+ } else {
90
+ return dropdownRef.current?.offsetParent !== null;
91
+ }
92
+ };
93
+ react.useEffect(() => {
94
+ if (!editorRef.current) {
95
+ return;
96
+ }
97
+ const initialDoc = lib.convertBylineModelToNode(initialValue);
98
+ const state = prosemirrorState.EditorState.create({
99
+ schema: schema.bylineEditorSchema,
100
+ plugins: [
101
+ prosemirrorDropcursor.dropCursor(),
102
+ plugins.clipboardPlugin(allowUntaggedContributors, contributorLimit),
103
+ prosemirrorHistory.history(),
104
+ plugins.keybindings(),
105
+ prosemirrorInvisibles.createInvisiblesPlugin([prosemirrorInvisibles.space, prosemirrorInvisibles.nbSpace], {
106
+ displayLineEndSelection: true,
107
+ shouldShowInvisibles: true
108
+ }),
109
+ plugins.bylinePlugin(),
110
+ placeholder.createPlaceholderPlugin(placeholder$1 ?? "Enter a byline...")
111
+ ],
112
+ doc: initialDoc
113
+ });
114
+ setCurrentDoc(initialDoc);
115
+ viewRef.current = new prosemirrorView.EditorView(editorRef.current, {
116
+ state,
117
+ editable: () => !readOnly,
118
+ attributes: {
119
+ role: "combobox",
120
+ "aria-label": "byline",
121
+ "aria-controls": "byline-dropdown",
122
+ "aria-expanded": "false",
123
+ "data-testid": "byline-input",
124
+ ...readOnly && { "aria-readonly": "true" }
125
+ },
126
+ handleDOMEvents: {
127
+ keydown: (view, event) => {
128
+ if (readOnly) {
129
+ return false;
130
+ }
131
+ if (event.key === "Escape") {
132
+ setShowDropdown(false);
133
+ return true;
134
+ }
135
+ if (event.key === "ArrowDown") {
136
+ if (checkDropdownVisibility()) {
137
+ event.preventDefault();
138
+ setCurrentOptionIndex((currentOptionIndex2) => {
139
+ return currentOptionIndex2 + 1;
140
+ });
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+ if (event.key === "ArrowUp") {
146
+ if (checkDropdownVisibility()) {
147
+ event.preventDefault();
148
+ setCurrentOptionIndex((currentOptionIndex2) => {
149
+ return currentOptionIndex2 - 1;
150
+ });
151
+ return true;
152
+ }
153
+ return false;
154
+ }
155
+ if (event.key === "Enter") {
156
+ if (checkDropdownVisibility()) {
157
+ event.preventDefault();
158
+ setEnterHit(true);
159
+ }
160
+ return false;
161
+ }
162
+ return false;
163
+ },
164
+ blur: (_view, event) => {
165
+ if (readOnly) {
166
+ return false;
167
+ }
168
+ if (!dropdownRef.current?.contains(
169
+ event.relatedTarget
170
+ )) {
171
+ setShowDropdown(false);
172
+ }
173
+ return false;
174
+ }
175
+ },
176
+ dispatchTransaction(tr) {
177
+ if (readOnly) {
178
+ if (tr.docChanged) {
179
+ return;
180
+ }
181
+ if (viewRef.current) {
182
+ const newState = viewRef.current.state.apply(tr);
183
+ viewRef.current.updateState(newState);
184
+ }
185
+ return;
186
+ }
187
+ if (viewRef.current?.hasFocus()) {
188
+ trackTypingFromStart(tr);
189
+ const newState = viewRef.current.state.apply(tr);
190
+ viewRef.current.updateState(newState);
191
+ const { selectedText } = lib.getCurrentText(
192
+ newState.doc,
193
+ newState.selection.from,
194
+ newState.selection.to,
195
+ isTypingFromStartRange.current
196
+ );
197
+ setCurrentText(selectedText);
198
+ const shouldShowDropdown = getShouldShowDropdown(
199
+ viewRef.current,
200
+ selectedText
201
+ );
202
+ if (shouldShowDropdown && searchContributors) {
203
+ void searchContributors(selectedText).then((contributors) => {
204
+ setTaggedContributors(contributors);
205
+ }).catch((error) => {
206
+ console.error(
207
+ "Error fetching tagged contributors:",
208
+ error
209
+ );
210
+ setTaggedContributors([]);
211
+ });
212
+ } else {
213
+ setTaggedContributors([]);
214
+ }
215
+ setShowDropdown(shouldShowDropdown);
216
+ if (tr.docChanged) {
217
+ setCurrentDoc(newState.doc);
218
+ handleSave(lib.convertNodeToBylineModel(newState.doc));
219
+ }
220
+ }
221
+ }
222
+ });
223
+ return () => {
224
+ viewRef.current?.destroy();
225
+ };
226
+ }, []);
227
+ react.useEffect(() => {
228
+ const numberOfOptions = taggedContributors.length + (allowUntaggedContributors ? 1 : 0);
229
+ if (numberOfOptions) {
230
+ if (currentOptionIndex < 0) {
231
+ setCurrentOptionIndex(numberOfOptions - 1);
232
+ } else {
233
+ setCurrentOptionIndex(currentOptionIndex % numberOfOptions);
234
+ }
235
+ }
236
+ if (showDropdown) {
237
+ const editor = document.querySelector("[role=combobox]");
238
+ editor?.setAttribute(
239
+ "aria-activedescendant",
240
+ `contributor-option-${currentOptionIndex}`
241
+ );
242
+ editor?.setAttribute("aria-expanded", "true");
243
+ }
244
+ }, [
245
+ currentOptionIndex,
246
+ showDropdown,
247
+ taggedContributors.length,
248
+ allowUntaggedContributors
249
+ ]);
250
+ react.useEffect(() => {
251
+ if (showDropdown && currentOptionIndex >= 0) {
252
+ const selectedOption = document.getElementById(
253
+ `contributor-option-${currentOptionIndex}`
254
+ );
255
+ if (selectedOption && dropdownRef.current) {
256
+ selectedOption.scrollIntoView({
257
+ behavior: "smooth",
258
+ block: "nearest"
259
+ });
260
+ }
261
+ }
262
+ }, [currentOptionIndex, showDropdown]);
263
+ react.useEffect(() => {
264
+ if (enterHit) {
265
+ if (allowUntaggedContributors && currentOptionIndex === taggedContributors.length) {
266
+ lib.addUntaggedContributor(
267
+ viewRef,
268
+ setShowDropdown,
269
+ contributorLimit,
270
+ isTypingFromStartRange.current
271
+ );
272
+ } else {
273
+ const contributorToAdd = taggedContributors[currentOptionIndex];
274
+ if (contributorToAdd) {
275
+ lib.addTaggedContributor(
276
+ contributorToAdd,
277
+ viewRef,
278
+ setShowDropdown,
279
+ contributorLimit,
280
+ isTypingFromStartRange.current
281
+ );
282
+ }
283
+ }
284
+ setEnterHit(false);
285
+ }
286
+ }, [
287
+ currentOptionIndex,
288
+ enterHit,
289
+ taggedContributors,
290
+ contributorLimit,
291
+ allowUntaggedContributors
292
+ ]);
293
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { css: styles.bylineContainerStyles, children: [
294
+ /* @__PURE__ */ jsxRuntime.jsx("div", { css: styles.bylineEditorStyles(theme?.editor), ref: editorRef, onBlur }),
295
+ /* @__PURE__ */ jsxRuntime.jsx(
296
+ "div",
297
+ {
298
+ ref: dropdownRef,
299
+ tabIndex: 0,
300
+ css: styles.dropdownContainerStyles(
301
+ showDropdown && // show the dropdown if there are tagged contributors to select or untagged contributors are allowed
302
+ (taggedContributors.length > 0 || !!allowUntaggedContributors),
303
+ theme?.dropdown
304
+ ),
305
+ children: /* @__PURE__ */ jsxRuntime.jsxs("ul", { id: "byline-dropdown", role: "listbox", css: styles.dropdownUlStyles, children: [
306
+ taggedContributors.map((contributor, i) => /* @__PURE__ */ jsxRuntime.jsx(
307
+ "li",
308
+ {
309
+ id: `contributor-option-${i}`,
310
+ role: "option",
311
+ "aria-selected": i === currentOptionIndex,
312
+ css: [
313
+ styles.dropdownLiStyles(theme),
314
+ i === currentOptionIndex && styles.selectedDropdownLiStyles(theme)
315
+ ],
316
+ onMouseMove: () => {
317
+ if (currentOptionIndex !== i) {
318
+ setCurrentOptionIndex(i);
319
+ }
320
+ },
321
+ onMouseDown: (e) => {
322
+ e.preventDefault();
323
+ lib.addTaggedContributor(
324
+ contributor,
325
+ viewRef,
326
+ setShowDropdown,
327
+ contributorLimit,
328
+ isTypingFromStartRange.current
329
+ );
330
+ },
331
+ children: contributor.internalLabel ?? contributor.label
332
+ },
333
+ contributor.tagId
334
+ )),
335
+ allowUntaggedContributors && /* @__PURE__ */ jsxRuntime.jsxs(
336
+ "li",
337
+ {
338
+ role: "option",
339
+ id: `contributor-option-${taggedContributors.length}`,
340
+ "aria-selected": currentOptionIndex === taggedContributors.length,
341
+ css: [
342
+ styles.dropdownLiStyles(theme),
343
+ currentOptionIndex === taggedContributors.length && styles.selectedDropdownLiStyles(theme)
344
+ ],
345
+ onMouseMove: () => {
346
+ if (currentOptionIndex !== taggedContributors.length) {
347
+ setCurrentOptionIndex(
348
+ taggedContributors.length
349
+ );
350
+ }
351
+ },
352
+ onMouseDown: (e) => {
353
+ e.preventDefault();
354
+ lib.addUntaggedContributor(
355
+ viewRef,
356
+ setShowDropdown,
357
+ contributorLimit,
358
+ isTypingFromStartRange.current
359
+ );
360
+ },
361
+ children: [
362
+ 'Add "',
363
+ currentText,
364
+ '" as untagged contributor'
365
+ ]
366
+ }
367
+ )
368
+ ] })
369
+ }
370
+ ),
371
+ enablePreview && /* @__PURE__ */ jsxRuntime.jsx(Preview.Preview, { doc: currentDoc })
372
+ ] });
373
+ };
374
+
375
+ exports.Byline = Byline;