@instructure/ui-spinner 11.7.7-snapshot-6 → 11.7.7-snapshot-8

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/CHANGELOG.md CHANGED
@@ -3,12 +3,11 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ## [11.7.7-snapshot-6](https://github.com/instructure/instructure-ui/compare/v11.7.6...v11.7.7-snapshot-6) (2026-09-23)
7
-
8
- **Note:** Version bump only for package @instructure/ui-spinner
9
-
6
+ ## [11.7.7-snapshot-8](https://github.com/instructure/instructure-ui/compare/v11.7.6...v11.7.7-snapshot-8) (2026-09-23)
10
7
 
8
+ ### Features
11
9
 
10
+ * **ui-spinner,ui-icons:** add ai and ai-on-color spinner variants ([cb3bd29](https://github.com/instructure/instructure-ui/commit/cb3bd2953f89f72a6d3a06c338a217eedce1a392))
12
11
 
13
12
 
14
13
  ## [11.7.6](https://github.com/instructure/instructure-ui/compare/v11.7.5...v11.7.6) (2026-09-14)
@@ -0,0 +1,44 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import { wrapCustomIcon } from '@instructure/ui-icons';
26
+
27
+ /**
28
+ * ---
29
+ * private: true
30
+ * ---
31
+ * The star path from packages/ui-icons/svg/Custom/igniteai-logo.svg, without
32
+ * the small sparkle. The star alone is centered in the viewBox, so it rotates
33
+ * evenly. If that logo changes, update this file too.
34
+ */
35
+ import { jsx as _jsx } from "@emotion/react/jsx-runtime";
36
+ const AISpinnerPaths = ({
37
+ color = 'currentColor'
38
+ }) => _jsx("path", {
39
+ d: "M11.0621 2.53451C11.3843 1.66389 12.6157 1.66389 12.9379 2.53451L15.0815 8.32767C15.1828 8.60139 15.3986 8.8172 15.6723 8.91848L21.4655 11.0621C22.3361 11.3843 22.3361 12.6157 21.4655 12.9379L15.6723 15.0815C15.3986 15.1828 15.1828 15.3986 15.0815 15.6723L12.9379 21.4655C12.6157 22.3361 11.3843 22.3361 11.0621 21.4655L8.91849 15.6723C8.8172 15.3986 8.60139 15.1828 8.32767 15.0815L2.53451 12.9379C1.66389 12.6157 1.66389 11.3843 2.53451 11.0621L8.32767 8.91849C8.60139 8.8172 8.8172 8.60139 8.91848 8.32767L11.0621 2.53451Z",
40
+ fill: color
41
+ });
42
+ const AISpinnerIcon = wrapCustomIcon(AISpinnerPaths, 'AISpinner', '0 0 24 24');
43
+ export default AISpinnerIcon;
44
+ export { AISpinnerIcon };
@@ -25,7 +25,9 @@
25
25
  import { useState, useEffect, forwardRef } from 'react';
26
26
  import { useStyleNew } from '@instructure/emotion';
27
27
  import { callRenderProp, omitProps, useDeterministicId } from '@instructure/ui-react-utils';
28
+ import { ScreenReaderContent } from '@instructure/ui-a11y-content';
28
29
  import { logError as error } from '@instructure/console';
30
+ import { AISpinnerIcon } from './AISpinnerIcon.js';
29
31
  import generateStyle from './styles.js';
30
32
  import { allowedProps } from './props.js';
31
33
 
@@ -34,7 +36,7 @@ import { allowedProps } from './props.js';
34
36
  category: components
35
37
  ---
36
38
  **/
37
- import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
39
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@emotion/react/jsx-runtime";
38
40
  const Spinner = /*#__PURE__*/forwardRef((props, ref) => {
39
41
  const {
40
42
  size = 'medium',
@@ -67,37 +69,57 @@ const Spinner = /*#__PURE__*/forwardRef((props, ref) => {
67
69
  }
68
70
  return undefined;
69
71
  }, [delay]);
72
+ const isAI = variant === 'ai' || variant === 'ai-on-color';
73
+ const renderCircle = () => _jsxs("svg", {
74
+ css: styles?.circle,
75
+ role: "img",
76
+ "aria-labelledby": titleId,
77
+ focusable: "false",
78
+ children: [_jsx("title", {
79
+ id: titleId,
80
+ children: callRenderProp(renderTitle)
81
+ }), _jsxs("g", {
82
+ role: "presentation",
83
+ children: [variant === 'default' && _jsx("circle", {
84
+ css: styles?.circleTrack,
85
+ cx: "50%",
86
+ cy: "50%",
87
+ r: styles?.radius
88
+ }), _jsx("circle", {
89
+ css: styles?.circleSpin,
90
+ cx: "50%",
91
+ cy: "50%",
92
+ r: styles?.radius
93
+ })]
94
+ })]
95
+ });
96
+
97
+ // The icon accepts only a string `title`, but `renderTitle` can be any node.
98
+ // So the label goes on the wrapping element instead.
99
+ const renderAIIcon = () => _jsxs(_Fragment, {
100
+ children: [_jsx(ScreenReaderContent, {
101
+ id: titleId,
102
+ children: callRenderProp(renderTitle)
103
+ }), _jsx("span", {
104
+ css: styles?.aiSpin,
105
+ children: _jsx(AISpinnerIcon, {
106
+ color: variant === 'ai' ? 'ai' : 'onColor'
107
+ })
108
+ })]
109
+ });
70
110
  const renderSpinner = () => {
71
111
  error(!!renderTitle, '[Spinner] The `renderTitle` prop is necessary for screen reader support.');
72
112
  const passthroughProps = omitProps(props, allowedProps);
73
113
  return _jsx("div", {
114
+ ...(isAI && {
115
+ role: 'img',
116
+ 'aria-labelledby': titleId
117
+ }),
74
118
  ...passthroughProps,
75
119
  css: styles?.spinner,
76
120
  ref: ref,
77
121
  "data-cid": "Spinner",
78
- children: _jsxs("svg", {
79
- css: styles?.circle,
80
- role: "img",
81
- "aria-labelledby": titleId,
82
- focusable: "false",
83
- children: [_jsx("title", {
84
- id: titleId,
85
- children: callRenderProp(renderTitle)
86
- }), _jsxs("g", {
87
- role: "presentation",
88
- children: [variant !== 'inverse' && _jsx("circle", {
89
- css: styles?.circleTrack,
90
- cx: "50%",
91
- cy: "50%",
92
- r: styles?.radius
93
- }), _jsx("circle", {
94
- css: styles?.circleSpin,
95
- cx: "50%",
96
- cy: "50%",
97
- r: styles?.radius
98
- })]
99
- })]
100
- })
122
+ children: isAI ? renderAIIcon() : renderCircle()
101
123
  });
102
124
  };
103
125
  return shouldRender ? renderSpinner() : null;
@@ -134,14 +134,10 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
134
134
  transformOrigin: `calc(${componentTheme.spinnerSizeLg} / 2) calc(${componentTheme.spinnerSizeLg} / 2)` //7em
135
135
  }
136
136
  };
137
- const circleSpinVariant = {
138
- default: {
139
- stroke: componentTheme.color
140
- },
141
- inverse: {
142
- stroke: componentTheme.inverseColor
143
- }
144
- };
137
+
138
+ // Only the circle variants use this. The AI variants render an icon instead,
139
+ // and take their color from the icon's own tokens.
140
+ const isInverse = variant === 'inverse';
145
141
  return {
146
142
  spinner: {
147
143
  label: 'spinner',
@@ -185,7 +181,25 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
185
181
  animationTimingFunction: 'ease',
186
182
  strokeDasharray: `calc(${radii[size]} * 2 * 3.14159 * 0.75) 1000px`,
187
183
  ...circleSpinSizes[size],
188
- ...circleSpinVariant[variant]
184
+ stroke: isInverse ? componentTheme.inverseColor : componentTheme.color
185
+ },
186
+ aiSpin: {
187
+ label: 'spinner__aiSpin',
188
+ display: 'block',
189
+ // The icon sits in an inline-block span. Without this, the line adds
190
+ // extra space below it, and the icon no longer rotates around its center.
191
+ lineHeight: 0,
192
+ ...circleSizes[size],
193
+ animationName: rotate,
194
+ animationDuration: '2.25s',
195
+ animationIterationCount: 'infinite',
196
+ animationTimingFunction: 'linear',
197
+ // The icon has no `size` prop, so its svg has no width or height. Use the
198
+ // same size as the circle, so the layout stays the same in every variant.
199
+ '& svg': {
200
+ display: 'block',
201
+ ...circleSizes[size]
202
+ }
189
203
  },
190
204
  radius: radii[size]
191
205
  };
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = exports.AISpinnerIcon = void 0;
7
+ var _wrapCustomIcon = require("@instructure/ui-icons/lib/custom/wrapCustomIcon.js");
8
+ var _jsxRuntime = require("@emotion/react/jsx-runtime");
9
+ /*
10
+ * The MIT License (MIT)
11
+ *
12
+ * Copyright (c) 2015 - present Instructure, Inc.
13
+ *
14
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ * of this software and associated documentation files (the "Software"), to deal
16
+ * in the Software without restriction, including without limitation the rights
17
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ * copies of the Software, and to permit persons to whom the Software is
19
+ * furnished to do so, subject to the following conditions:
20
+ *
21
+ * The above copyright notice and this permission notice shall be included in all
22
+ * copies or substantial portions of the Software.
23
+ *
24
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ * SOFTWARE.
31
+ */
32
+
33
+ /**
34
+ * ---
35
+ * private: true
36
+ * ---
37
+ * The star path from packages/ui-icons/svg/Custom/igniteai-logo.svg, without
38
+ * the small sparkle. The star alone is centered in the viewBox, so it rotates
39
+ * evenly. If that logo changes, update this file too.
40
+ */const AISpinnerPaths = ({
41
+ color = 'currentColor'
42
+ }) => (0, _jsxRuntime.jsx)("path", {
43
+ d: "M11.0621 2.53451C11.3843 1.66389 12.6157 1.66389 12.9379 2.53451L15.0815 8.32767C15.1828 8.60139 15.3986 8.8172 15.6723 8.91848L21.4655 11.0621C22.3361 11.3843 22.3361 12.6157 21.4655 12.9379L15.6723 15.0815C15.3986 15.1828 15.1828 15.3986 15.0815 15.6723L12.9379 21.4655C12.6157 22.3361 11.3843 22.3361 11.0621 21.4655L8.91849 15.6723C8.8172 15.3986 8.60139 15.1828 8.32767 15.0815L2.53451 12.9379C1.66389 12.6157 1.66389 11.3843 2.53451 11.0621L8.32767 8.91849C8.60139 8.8172 8.8172 8.60139 8.91848 8.32767L11.0621 2.53451Z",
44
+ fill: color
45
+ });
46
+ const AISpinnerIcon = exports.AISpinnerIcon = (0, _wrapCustomIcon.wrapCustomIcon)(AISpinnerPaths, 'AISpinner', '0 0 24 24');
47
+ var _default = exports.default = AISpinnerIcon;
@@ -10,7 +10,9 @@ var _emotion = require("@instructure/emotion");
10
10
  var _callRenderProp = require("@instructure/ui-react-utils/lib/callRenderProp.js");
11
11
  var _omitProps = require("@instructure/ui-react-utils/lib/omitProps.js");
12
12
  var _useDeterministicId = require("@instructure/ui-react-utils/lib/DeterministicIdContext/useDeterministicId.js");
13
+ var _ScreenReaderContent = require("@instructure/ui-a11y-content/lib/ScreenReaderContent");
13
14
  var _console = require("@instructure/console");
15
+ var _AISpinnerIcon = require("./AISpinnerIcon.js");
14
16
  var _styles = _interopRequireDefault(require("./styles.js"));
15
17
  var _props = require("./props.js");
16
18
  var _jsxRuntime = require("@emotion/react/jsx-runtime");
@@ -74,37 +76,57 @@ category: components
74
76
  }
75
77
  return undefined;
76
78
  }, [delay]);
79
+ const isAI = variant === 'ai' || variant === 'ai-on-color';
80
+ const renderCircle = () => (0, _jsxRuntime.jsxs)("svg", {
81
+ css: styles?.circle,
82
+ role: "img",
83
+ "aria-labelledby": titleId,
84
+ focusable: "false",
85
+ children: [(0, _jsxRuntime.jsx)("title", {
86
+ id: titleId,
87
+ children: (0, _callRenderProp.callRenderProp)(renderTitle)
88
+ }), (0, _jsxRuntime.jsxs)("g", {
89
+ role: "presentation",
90
+ children: [variant === 'default' && (0, _jsxRuntime.jsx)("circle", {
91
+ css: styles?.circleTrack,
92
+ cx: "50%",
93
+ cy: "50%",
94
+ r: styles?.radius
95
+ }), (0, _jsxRuntime.jsx)("circle", {
96
+ css: styles?.circleSpin,
97
+ cx: "50%",
98
+ cy: "50%",
99
+ r: styles?.radius
100
+ })]
101
+ })]
102
+ });
103
+
104
+ // The icon accepts only a string `title`, but `renderTitle` can be any node.
105
+ // So the label goes on the wrapping element instead.
106
+ const renderAIIcon = () => (0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
107
+ children: [(0, _jsxRuntime.jsx)(_ScreenReaderContent.ScreenReaderContent, {
108
+ id: titleId,
109
+ children: (0, _callRenderProp.callRenderProp)(renderTitle)
110
+ }), (0, _jsxRuntime.jsx)("span", {
111
+ css: styles?.aiSpin,
112
+ children: (0, _jsxRuntime.jsx)(_AISpinnerIcon.AISpinnerIcon, {
113
+ color: variant === 'ai' ? 'ai' : 'onColor'
114
+ })
115
+ })]
116
+ });
77
117
  const renderSpinner = () => {
78
118
  (0, _console.logError)(!!renderTitle, '[Spinner] The `renderTitle` prop is necessary for screen reader support.');
79
119
  const passthroughProps = (0, _omitProps.omitProps)(props, _props.allowedProps);
80
120
  return (0, _jsxRuntime.jsx)("div", {
121
+ ...(isAI && {
122
+ role: 'img',
123
+ 'aria-labelledby': titleId
124
+ }),
81
125
  ...passthroughProps,
82
126
  css: styles?.spinner,
83
127
  ref: ref,
84
128
  "data-cid": "Spinner",
85
- children: (0, _jsxRuntime.jsxs)("svg", {
86
- css: styles?.circle,
87
- role: "img",
88
- "aria-labelledby": titleId,
89
- focusable: "false",
90
- children: [(0, _jsxRuntime.jsx)("title", {
91
- id: titleId,
92
- children: (0, _callRenderProp.callRenderProp)(renderTitle)
93
- }), (0, _jsxRuntime.jsxs)("g", {
94
- role: "presentation",
95
- children: [variant !== 'inverse' && (0, _jsxRuntime.jsx)("circle", {
96
- css: styles?.circleTrack,
97
- cx: "50%",
98
- cy: "50%",
99
- r: styles?.radius
100
- }), (0, _jsxRuntime.jsx)("circle", {
101
- css: styles?.circleSpin,
102
- cx: "50%",
103
- cy: "50%",
104
- r: styles?.radius
105
- })]
106
- })]
107
- })
129
+ children: isAI ? renderAIIcon() : renderCircle()
108
130
  });
109
131
  };
110
132
  return shouldRender ? renderSpinner() : null;
@@ -140,14 +140,10 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
140
140
  transformOrigin: `calc(${componentTheme.spinnerSizeLg} / 2) calc(${componentTheme.spinnerSizeLg} / 2)` //7em
141
141
  }
142
142
  };
143
- const circleSpinVariant = {
144
- default: {
145
- stroke: componentTheme.color
146
- },
147
- inverse: {
148
- stroke: componentTheme.inverseColor
149
- }
150
- };
143
+
144
+ // Only the circle variants use this. The AI variants render an icon instead,
145
+ // and take their color from the icon's own tokens.
146
+ const isInverse = variant === 'inverse';
151
147
  return {
152
148
  spinner: {
153
149
  label: 'spinner',
@@ -191,7 +187,25 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
191
187
  animationTimingFunction: 'ease',
192
188
  strokeDasharray: `calc(${radii[size]} * 2 * 3.14159 * 0.75) 1000px`,
193
189
  ...circleSpinSizes[size],
194
- ...circleSpinVariant[variant]
190
+ stroke: isInverse ? componentTheme.inverseColor : componentTheme.color
191
+ },
192
+ aiSpin: {
193
+ label: 'spinner__aiSpin',
194
+ display: 'block',
195
+ // The icon sits in an inline-block span. Without this, the line adds
196
+ // extra space below it, and the icon no longer rotates around its center.
197
+ lineHeight: 0,
198
+ ...circleSizes[size],
199
+ animationName: rotate,
200
+ animationDuration: '2.25s',
201
+ animationIterationCount: 'infinite',
202
+ animationTimingFunction: 'linear',
203
+ // The icon has no `size` prop, so its svg has no width or height. Use the
204
+ // same size as the circle, so the layout stays the same in every variant.
205
+ '& svg': {
206
+ display: 'block',
207
+ ...circleSizes[size]
208
+ }
195
209
  },
196
210
  radius: radii[size]
197
211
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-spinner",
3
- "version": "11.7.7-snapshot-6",
3
+ "version": "11.7.7-snapshot-8",
4
4
  "description": "A spinner/loading component",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -15,20 +15,22 @@
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
17
  "@babel/runtime": "^7.29.7",
18
- "@instructure/emotion": "11.7.7-snapshot-6",
19
- "@instructure/console": "11.7.7-snapshot-6",
20
- "@instructure/ui-color-utils": "11.7.7-snapshot-6",
21
- "@instructure/shared-types": "11.7.7-snapshot-6",
22
- "@instructure/ui-react-utils": "11.7.7-snapshot-6",
23
- "@instructure/ui-utils": "11.7.7-snapshot-6",
24
- "@instructure/ui-themes": "11.7.7-snapshot-6",
25
- "@instructure/uid": "11.7.7-snapshot-6",
26
- "@instructure/ui-view": "11.7.7-snapshot-6"
18
+ "@instructure/console": "11.7.7-snapshot-8",
19
+ "@instructure/emotion": "11.7.7-snapshot-8",
20
+ "@instructure/ui-a11y-content": "11.7.7-snapshot-8",
21
+ "@instructure/shared-types": "11.7.7-snapshot-8",
22
+ "@instructure/ui-color-utils": "11.7.7-snapshot-8",
23
+ "@instructure/ui-icons": "11.7.7-snapshot-8",
24
+ "@instructure/ui-react-utils": "11.7.7-snapshot-8",
25
+ "@instructure/ui-themes": "11.7.7-snapshot-8",
26
+ "@instructure/ui-view": "11.7.7-snapshot-8",
27
+ "@instructure/uid": "11.7.7-snapshot-8",
28
+ "@instructure/ui-utils": "11.7.7-snapshot-8"
27
29
  },
28
30
  "devDependencies": {
29
31
  "vitest": "^5.0.0",
30
- "@instructure/ui-babel-preset": "11.7.7-snapshot-6",
31
- "@instructure/ui-axe-check": "11.7.7-snapshot-6"
32
+ "@instructure/ui-axe-check": "11.7.7-snapshot-8",
33
+ "@instructure/ui-babel-preset": "11.7.7-snapshot-8"
32
34
  },
33
35
  "peerDependencies": {
34
36
  "react": ">=18 <=19"
@@ -0,0 +1,45 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import { wrapCustomIcon } from '@instructure/ui-icons'
26
+
27
+ /**
28
+ * ---
29
+ * private: true
30
+ * ---
31
+ * The star path from packages/ui-icons/svg/Custom/igniteai-logo.svg, without
32
+ * the small sparkle. The star alone is centered in the viewBox, so it rotates
33
+ * evenly. If that logo changes, update this file too.
34
+ */
35
+ const AISpinnerPaths = ({ color = 'currentColor' }: { color?: string }) => (
36
+ <path
37
+ d="M11.0621 2.53451C11.3843 1.66389 12.6157 1.66389 12.9379 2.53451L15.0815 8.32767C15.1828 8.60139 15.3986 8.8172 15.6723 8.91848L21.4655 11.0621C22.3361 11.3843 22.3361 12.6157 21.4655 12.9379L15.6723 15.0815C15.3986 15.1828 15.1828 15.3986 15.0815 15.6723L12.9379 21.4655C12.6157 22.3361 11.3843 22.3361 11.0621 21.4655L8.91849 15.6723C8.8172 15.3986 8.60139 15.1828 8.32767 15.0815L2.53451 12.9379C1.66389 12.6157 1.66389 11.3843 2.53451 11.0621L8.32767 8.91849C8.60139 8.8172 8.8172 8.60139 8.91848 8.32767L11.0621 2.53451Z"
38
+ fill={color}
39
+ />
40
+ )
41
+
42
+ const AISpinnerIcon = wrapCustomIcon(AISpinnerPaths, 'AISpinner', '0 0 24 24')
43
+
44
+ export default AISpinnerIcon
45
+ export { AISpinnerIcon }
@@ -28,7 +28,7 @@ dark backgrounds.
28
28
  ---
29
29
  type: example
30
30
  ---
31
- <View background="primary-inverse" as="div" >
31
+ <View background="info" as="div" >
32
32
  <Spinner renderTitle="Loading" variant="inverse" />
33
33
  </View>
34
34
  ```
@@ -61,3 +61,23 @@ type: example
61
61
  <Spinner renderTitle={() => "Hello world"} />
62
62
  </div>
63
63
  ```
64
+
65
+ ### AI variants
66
+
67
+ Use the `ai` variants to show that an AI feature is working. Use `ai-on-color`
68
+ on colored backgrounds.
69
+
70
+ ```js
71
+ ---
72
+ type: example
73
+ ---
74
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
75
+ <Spinner renderTitle="Loading" variant="ai" size="x-small" />
76
+ <Spinner renderTitle="Loading" variant="ai" size="small" />
77
+ <Spinner renderTitle="Loading" variant="ai" />
78
+ <Spinner renderTitle="Loading" variant="ai" size="large" />
79
+ <View background="info" as="div" padding="small">
80
+ <Spinner renderTitle="Loading" variant="ai-on-color" />
81
+ </View>
82
+ </div>
83
+ ```
@@ -30,8 +30,10 @@ import {
30
30
  omitProps,
31
31
  useDeterministicId
32
32
  } from '@instructure/ui-react-utils'
33
+ import { ScreenReaderContent } from '@instructure/ui-a11y-content'
33
34
  import { logError as error } from '@instructure/console'
34
35
 
36
+ import { AISpinnerIcon } from './AISpinnerIcon.js'
35
37
  import generateStyle from './styles.js'
36
38
  import type { SpinnerProps } from './props'
37
39
  import { allowedProps } from './props.js'
@@ -78,6 +80,48 @@ const Spinner = forwardRef<HTMLDivElement, SpinnerProps>((props, ref) => {
78
80
  return undefined
79
81
  }, [delay])
80
82
 
83
+ const isAI = variant === 'ai' || variant === 'ai-on-color'
84
+
85
+ const renderCircle = () => (
86
+ <svg
87
+ css={styles?.circle}
88
+ role="img"
89
+ aria-labelledby={titleId}
90
+ focusable="false"
91
+ >
92
+ <title id={titleId}>{callRenderProp(renderTitle)}</title>
93
+ <g role="presentation">
94
+ {variant === 'default' && (
95
+ <circle
96
+ css={styles?.circleTrack}
97
+ cx="50%"
98
+ cy="50%"
99
+ r={styles?.radius as string}
100
+ />
101
+ )}
102
+ <circle
103
+ css={styles?.circleSpin}
104
+ cx="50%"
105
+ cy="50%"
106
+ r={styles?.radius as string}
107
+ />
108
+ </g>
109
+ </svg>
110
+ )
111
+
112
+ // The icon accepts only a string `title`, but `renderTitle` can be any node.
113
+ // So the label goes on the wrapping element instead.
114
+ const renderAIIcon = () => (
115
+ <>
116
+ <ScreenReaderContent id={titleId}>
117
+ {callRenderProp(renderTitle)}
118
+ </ScreenReaderContent>
119
+ <span css={styles?.aiSpin}>
120
+ <AISpinnerIcon color={variant === 'ai' ? 'ai' : 'onColor'} />
121
+ </span>
122
+ </>
123
+ )
124
+
81
125
  const renderSpinner = () => {
82
126
  error(
83
127
  !!renderTitle,
@@ -88,35 +132,13 @@ const Spinner = forwardRef<HTMLDivElement, SpinnerProps>((props, ref) => {
88
132
 
89
133
  return (
90
134
  <div
135
+ {...(isAI && { role: 'img', 'aria-labelledby': titleId })}
91
136
  {...passthroughProps}
92
137
  css={styles?.spinner}
93
138
  ref={ref}
94
139
  data-cid="Spinner"
95
140
  >
96
- <svg
97
- css={styles?.circle}
98
- role="img"
99
- aria-labelledby={titleId}
100
- focusable="false"
101
- >
102
- <title id={titleId}>{callRenderProp(renderTitle)}</title>
103
- <g role="presentation">
104
- {variant !== 'inverse' && (
105
- <circle
106
- css={styles?.circleTrack}
107
- cx="50%"
108
- cy="50%"
109
- r={styles?.radius as string}
110
- />
111
- )}
112
- <circle
113
- css={styles?.circleSpin}
114
- cx="50%"
115
- cy="50%"
116
- r={styles?.radius as string}
117
- />
118
- </g>
119
- </svg>
141
+ {isAI ? renderAIIcon() : renderCircle()}
120
142
  </div>
121
143
  )
122
144
  }
@@ -52,9 +52,11 @@ type SpinnerOwnProps = {
52
52
  */
53
53
  size?: 'x-small' | 'small' | 'medium' | 'large'
54
54
  /**
55
- * Different color schemes for use with light or dark backgrounds
55
+ * Different color schemes for use with light or dark backgrounds.
56
+ * The `ai` variants show the AI icon instead of the circle. `ai` uses the
57
+ * theme's AI gradient, and `ai-on-color` is for colored backgrounds.
56
58
  */
57
- variant?: 'default' | 'inverse'
59
+ variant?: 'default' | 'inverse' | 'ai' | 'ai-on-color'
58
60
  }
59
61
 
60
62
  type PropKeys = keyof SpinnerOwnProps
@@ -67,7 +69,7 @@ type SpinnerProps = SpinnerOwnProps &
67
69
  WithDeterministicIdProps
68
70
 
69
71
  type SpinnerStyle = ComponentStyle<
70
- 'spinner' | 'circle' | 'circleTrack' | 'circleSpin' | 'radius'
72
+ 'spinner' | 'circle' | 'circleTrack' | 'circleSpin' | 'radius' | 'aiSpin'
71
73
  >
72
74
 
73
75
  const allowedProps: AllowedPropKeys = [