@visns-studio/visns-components 6.0.4 → 6.0.5

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/package.json CHANGED
@@ -91,7 +91,7 @@
91
91
  "react-dom": "^17.0.0 || ^18.0.0"
92
92
  },
93
93
  "name": "@visns-studio/visns-components",
94
- "version": "6.0.4",
94
+ "version": "6.0.5",
95
95
  "description": "Various packages to assist in the development of our Custom Applications.",
96
96
  "main": "src/index.js",
97
97
  "files": [
@@ -236,6 +236,34 @@ const loadData = async (
236
236
  }
237
237
  };
238
238
 
239
+ /**
240
+ * How tall the grid should be.
241
+ *
242
+ * It used to claim 70% of the viewport (or 550px) for any result set larger
243
+ * than eight rows, whatever the actual row count — so 18 rows on a tall
244
+ * monitor left several hundred pixels of empty white below the last one.
245
+ *
246
+ * Height now follows the content and only falls back to a viewport share when
247
+ * the rows genuinely overflow, which is the point at which a tall scroller is
248
+ * useful rather than just empty.
249
+ */
250
+ const ROW_HEIGHT = 34;
251
+ const GRID_CHROME = ROW_HEIGHT * 2 + 60; // header + filter row + footer
252
+
253
+ function fitToRows(dataCount, windowHeight) {
254
+ const roomy = Math.max(windowHeight * 0.7, 550);
255
+
256
+ if (!dataCount || dataCount <= 0) {
257
+ // Nothing to show: enough for the empty state, not a blank screen.
258
+ return 300;
259
+ }
260
+
261
+ const needed = dataCount * ROW_HEIGHT + GRID_CHROME;
262
+
263
+ // Never taller than the rows require, never so short it feels cramped.
264
+ return Math.min(roomy, Math.max(needed, 260));
265
+ }
266
+
239
267
  const DataGrid = forwardRef(
240
268
  (
241
269
  {
@@ -4380,13 +4408,7 @@ const DataGrid = forwardRef(
4380
4408
  minHeight:
4381
4409
  gridHeight && gridHeight > 0
4382
4410
  ? gridHeight
4383
- : dataCount > 5
4384
- ? dataCount > 8
4385
- ? windowHeight * 0.7 > 550
4386
- ? windowHeight * 0.7
4387
- : 550
4388
- : 450
4389
- : 400,
4411
+ : fitToRows(dataCount, windowHeight),
4390
4412
  boxShadow: 'none',
4391
4413
  ...(hoverColor ? { '--hover-color': hoverColor } : {}),
4392
4414
  });
@@ -4397,11 +4419,7 @@ const DataGrid = forwardRef(
4397
4419
  useEffect(() => {
4398
4420
  const getMinimumHeight = () => {
4399
4421
  if (gridHeight > 0) return gridHeight;
4400
- if (dataCount > 0) {
4401
- if (dataCount <= 5) return 400;
4402
- if (dataCount <= 8) return 450;
4403
- }
4404
- return Math.max(window.innerHeight * 0.7, 550);
4422
+ return fitToRows(dataCount, window.innerHeight);
4405
4423
  };
4406
4424
 
4407
4425
  const minHeight = getMinimumHeight();
@@ -1,19 +1,41 @@
1
1
  import '../styles/global.css';
2
2
 
3
- import React, { useState } from 'react';
3
+ import React, { useEffect, useRef, useState } from 'react';
4
4
  import { Link, useLocation, useNavigate } from 'react-router-dom';
5
- import Reveal from '../utils/Reveal';
6
5
  import { toast } from 'react-toastify';
7
- import { User, Lock } from 'lucide-react';
6
+ import { Eye, EyeOff } from 'lucide-react';
8
7
 
9
8
  import CustomFetch from '../Fetch';
10
9
 
11
10
  import styles from '../styles/Login.module.scss';
12
11
 
13
- const Login = ({ logo, providers, setSystemAuth, setUserProfile, config = {} }) => {
12
+ const Login = ({
13
+ logo,
14
+ loginBg,
15
+ providers,
16
+ setSystemAuth,
17
+ setUserProfile,
18
+ config = {},
19
+ }) => {
14
20
  const location = useLocation();
15
21
  const navigate = useNavigate();
16
22
  const [isLoading, setIsLoading] = useState(false);
23
+ const [showPassword, setShowPassword] = useState(false);
24
+
25
+ // Collapsed behind a button, since Microsoft is how nearly everyone signs
26
+ // in. Open from the start when there is no SSO provider configured —
27
+ // otherwise the only way in would be hidden behind a disclosure.
28
+ const ssoAvailable = Array.isArray(providers) && providers.length > 0;
29
+ const [showEmail, setShowEmail] = useState(!ssoAvailable);
30
+ const emailRef = useRef(null);
31
+
32
+ // Opening the form should put the cursor where typing starts, rather than
33
+ // leaving it on the button that opened it.
34
+ useEffect(() => {
35
+ if (showEmail && ssoAvailable) {
36
+ emailRef.current?.focus();
37
+ }
38
+ }, [showEmail, ssoAvailable]);
17
39
 
18
40
  const [auth, setAuth] = useState({
19
41
  email: '',
@@ -32,43 +54,37 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile, config = {} })
32
54
  return null;
33
55
  }
34
56
 
35
- let content = <div className={styles.divider}>OR</div>;
36
-
37
- if (providers) {
38
- providers.forEach((provider) => {
39
- switch (provider) {
40
- case 'azure':
41
- content = (
42
- <>
43
- {content}
44
- <div
45
- className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem}`}
46
- >
47
- <button
48
- className={styles.ssoButton}
49
- onClick={(e) => {
50
- e.preventDefault();
51
- window.location.href =
52
- '/auth/azure';
53
- }}
54
- >
55
- <img
56
- src={
57
- 'https://d16lktya8ojp5z.cloudfront.net/generic/images/microsoft.png'
58
- }
59
- width="18px"
60
- height="18px"
61
- alt="Microsoft logo"
62
- />
63
- Sign in with Microsoft
64
- </button>
65
- </div>
66
- </>
67
- );
68
- break;
69
- }
70
- });
71
- }
57
+ let content = null;
58
+
59
+ providers.forEach((provider) => {
60
+ switch (provider) {
61
+ case 'azure':
62
+ content = (
63
+ <button
64
+ type="button"
65
+ className={styles.sso}
66
+ onClick={(e) => {
67
+ e.preventDefault();
68
+ window.location.href = '/auth/azure';
69
+ }}
70
+ >
71
+ {/* The Microsoft mark keeps its own light tile.
72
+ Its four squares are drawn for a white ground
73
+ and sit awkwardly straight on navy. */}
74
+ <span className={styles.ssoMark}>
75
+ <img
76
+ src="https://d16lktya8ojp5z.cloudfront.net/generic/images/microsoft.png"
77
+ width="18"
78
+ height="18"
79
+ alt=""
80
+ />
81
+ </span>
82
+ Sign in with Microsoft
83
+ </button>
84
+ );
85
+ break;
86
+ }
87
+ });
72
88
 
73
89
  return content;
74
90
  };
@@ -162,141 +178,153 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile, config = {} })
162
178
  };
163
179
 
164
180
  return (
165
- <div className={styles.lcontainer}>
166
- <div className={styles.lwrap}>
167
- <div className={styles.logincontainer}>
168
- <form onSubmit={handleSubmit}>
169
- <Reveal effect="fadeInUp">
170
- <div className={styles.login}>
171
- <img
172
- src={logo}
173
- alt="CRM"
174
- className={styles.loginlogo}
175
- />
176
- <h2
177
- style={{
178
- textAlign: 'center',
179
- marginBottom: '1.5rem',
180
- color: '#111827',
181
- fontWeight: '600',
182
- fontSize: '1.5rem',
183
- }}
184
- >
185
- Sign in to your account
186
- </h2>
187
- <div
188
- className={`${styles.formItem} ${styles.fwItem}`}
189
- >
190
- <label className={styles.fi__label}>
191
- <input
192
- type="text"
193
- name="email"
194
- value={auth.email}
195
- onChange={handleChange}
196
- tabIndex="1"
197
- className={authClass.username}
198
- placeholder=" "
199
- />
200
- <span className={styles.fi__span}>
201
- Email
202
- </span>
203
- <small>
204
- <User strokeWidth={2} size={18} />
205
- </small>
206
- </label>
207
- </div>
208
- <div
209
- className={`${styles.formItem} ${styles.fwItem}`}
210
- >
211
- <label className={styles.fi__label}>
212
- <input
213
- type="password"
214
- name="password"
215
- value={auth.password}
216
- onChange={handleChange}
217
- tabIndex="2"
218
- className={authClass.password}
219
- placeholder=" "
220
- />
221
- <span className={styles.fi__span}>
222
- Password
223
- </span>
224
- <small>
225
- <Lock
226
- strokeWidth={2}
227
- size={18}
228
- />
229
- </small>
230
- </label>
231
- </div>
232
- <div
233
- className={`${styles.formItem} ${styles.fwItem}`}
234
- >
235
- <div className={styles.rememberMeContainer}>
236
- <div
237
- style={{
238
- display: 'flex',
239
- alignItems: 'center',
240
- }}
241
- >
242
- <input
243
- type="checkbox"
244
- id="rememberMe"
245
- name="remember"
246
- checked={auth.remember}
247
- onChange={handleChange}
248
- className={
249
- styles.rememberMeCheckbox
250
- }
251
- />
252
- <label
253
- htmlFor="rememberMe"
254
- className={
255
- styles.rememberMeLabel
256
- }
257
- >
258
- Remember me
259
- </label>
260
- </div>
261
- {config.passwordReset !== false && (
262
- <div>
263
- <Link
264
- to="/reset"
265
- style={{
266
- color: 'var(--primary-color, #4f46e5)',
267
- textDecoration: 'none',
268
- fontWeight: '500',
269
- fontSize: '0.875rem',
270
- }}
271
- className={
272
- styles.forgotPasswordLink
273
- }
274
- >
275
- Forgot password?
276
- </Link>
277
- </div>
278
- )}
279
- </div>
280
- </div>
281
- <div
282
- className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem}`}
283
- >
284
- <button
285
- className={styles.btn}
286
- type="submit"
287
- disabled={isLoading}
288
- >
289
- {isLoading
290
- ? 'Signing in...'
291
- : 'Sign in'}
292
- </button>
293
- </div>
294
- {renderSsoButton()}
295
- </div>
296
- </Reveal>
297
- </form>
181
+ <div className={styles.auth}>
182
+ {/* The company's own work, not decoration. It is the reason the
183
+ rest of this system exists, so it carries the brand side and
184
+ the form is left to be a form. */}
185
+ <aside
186
+ className={styles.brand}
187
+ style={
188
+ loginBg ? { backgroundImage: `url(${loginBg})` } : undefined
189
+ }
190
+ aria-hidden="true"
191
+ >
192
+ <div className={styles.brandInk}>
193
+ <p className={styles.eyebrow}>Prime Builders</p>
194
+ <p className={styles.brandLine}>Prime Projects</p>
195
+ <p className={styles.brandSub}>
196
+ Job tracking, inspections, site forms and labour
197
+ scheduling — in one place.
198
+ </p>
199
+ </div>
200
+ </aside>
201
+
202
+ <main className={styles.panel}>
203
+ <div className={styles.panelInner}>
204
+ <img src={logo} alt="Prime Builders" className={styles.logo} />
205
+
206
+ <h1 className={styles.title}>Sign in</h1>
207
+ <p className={styles.subtitle}>
208
+ Use your Prime Microsoft account to continue.
209
+ </p>
210
+
211
+ {/* Primary: this is how Prime staff actually sign in. The
212
+ email form stays available underneath for accounts
213
+ without a Microsoft sign-in. */}
214
+ {renderSsoButton()}
215
+
216
+ {ssoAvailable && !showEmail && (
217
+ <button
218
+ type="button"
219
+ className={styles.altToggle}
220
+ onClick={() => setShowEmail(true)}
221
+ aria-expanded="false"
222
+ aria-controls="email-signin"
223
+ >
224
+ Sign in with email instead
225
+ </button>
226
+ )}
227
+
228
+ {showEmail && ssoAvailable && (
229
+ <div className={styles.divider}>
230
+ <span>or sign in with email</span>
231
+ </div>
232
+ )}
233
+
234
+ {/* A grid row animating 0fr -> 1fr, so it opens to whatever
235
+ height the form needs without hardcoding one. */}
236
+ <div
237
+ id="email-signin"
238
+ className={`${styles.collapse} ${
239
+ showEmail ? styles.collapseOpen : ''
240
+ }`}
241
+ >
242
+ <div className={styles.collapseInner}>
243
+ <form onSubmit={handleSubmit} className={styles.form}>
244
+ <div className={styles.field}>
245
+ <input
246
+ id="login-email"
247
+ ref={emailRef}
248
+ type="text"
249
+ name="email"
250
+ value={auth.email}
251
+ onChange={handleChange}
252
+ tabIndex="1"
253
+ className={authClass.username}
254
+ autoComplete="username"
255
+ placeholder=" "
256
+ />
257
+ <label htmlFor="login-email">Email</label>
258
+ </div>
259
+
260
+ <div className={styles.field}>
261
+ <input
262
+ id="login-password"
263
+ type={showPassword ? 'text' : 'password'}
264
+ name="password"
265
+ value={auth.password}
266
+ onChange={handleChange}
267
+ tabIndex="2"
268
+ className={authClass.password}
269
+ autoComplete="current-password"
270
+ placeholder=" "
271
+ />
272
+ <label htmlFor="login-password">Password</label>
273
+ {/* Typing a password blind on a phone in the sun is
274
+ where most failed sign-ins come from. */}
275
+ <button
276
+ type="button"
277
+ className={styles.reveal}
278
+ onClick={() => setShowPassword((on) => !on)}
279
+ aria-label={
280
+ showPassword
281
+ ? 'Hide password'
282
+ : 'Show password'
283
+ }
284
+ tabIndex="-1"
285
+ >
286
+ {showPassword ? (
287
+ <EyeOff size={18} strokeWidth={1.9} />
288
+ ) : (
289
+ <Eye size={18} strokeWidth={1.9} />
290
+ )}
291
+ </button>
292
+ </div>
293
+
294
+ <div className={styles.row}>
295
+ <label className={styles.remember}>
296
+ <input
297
+ type="checkbox"
298
+ name="remember"
299
+ checked={auth.remember}
300
+ onChange={handleChange}
301
+ />
302
+ Remember me
303
+ </label>
304
+
305
+ {config.passwordReset !== false && (
306
+ <Link to="/reset" className={styles.link}>
307
+ Forgot password?
308
+ </Link>
309
+ )}
310
+ </div>
311
+
312
+ <button
313
+ className={styles.submit}
314
+ type="submit"
315
+ disabled={isLoading}
316
+ >
317
+ {isLoading ? 'Signing in…' : 'Sign in with email'}
318
+ </button>
319
+ </form>
320
+ </div>
321
+ </div>
322
+
323
+ <p className={styles.foot}>
324
+ Prime Builders · Perth, Western Australia
325
+ </p>
298
326
  </div>
299
- </div>
327
+ </main>
300
328
  </div>
301
329
  );
302
330
  };
@@ -152,6 +152,40 @@ export const renderBooleanColumn = ({
152
152
  render: ({ data }) => {
153
153
  const rawValue = data[column.id];
154
154
  const formattedValue = formatCellContent(rawValue, { ...column, type: 'boolean' });
155
+
156
+ // Coloured pill variant — opt in via column.badge, matching the
157
+ // option renderer. A yes/no column rendered as plain text reads
158
+ // the same as every other cell, so the state has to be read
159
+ // rather than scanned. Without column.badge nothing changes.
160
+ if (column.badge) {
161
+ const on = Boolean(rawValue) && rawValue !== '0';
162
+ const palette = on
163
+ ? (column.badgeOn || { bg: '#eaf6ef', fg: '#1f6b41' })
164
+ : (column.badgeOff || { bg: '#f1f1ee', fg: '#6b7688' });
165
+
166
+ return (
167
+ <CellWithTooltip value={rawValue} columnType="boolean">
168
+ <span
169
+ style={{
170
+ display: 'inline-block',
171
+ padding: '2px 10px',
172
+ borderRadius: '999px',
173
+ fontSize: '11px',
174
+ fontWeight: 600,
175
+ lineHeight: 1.6,
176
+ letterSpacing: '0.02em',
177
+ textTransform: 'uppercase',
178
+ whiteSpace: 'nowrap',
179
+ backgroundColor: palette.bg,
180
+ color: palette.fg,
181
+ }}
182
+ >
183
+ {formattedValue}
184
+ </span>
185
+ </CellWithTooltip>
186
+ );
187
+ }
188
+
155
189
  return (
156
190
  <CellWithTooltip value={rawValue} columnType="boolean">
157
191
  <span>{formattedValue}</span>