@the-portland-company/shell 0.87.1 → 0.89.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.
- package/CHANGELOG.md +22 -0
- package/README.md +51 -0
- package/bin/shell-version-check.mjs +117 -0
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +1 -1
- package/dist/app-chrome.d.ts +1 -1
- package/dist/app-chrome.js +3 -3
- package/dist/chrome.css +20 -0
- package/dist/{chunk-PRZP2SB6.js → chunk-J23MTK56.js} +562 -5
- package/dist/chunk-J23MTK56.js.map +1 -0
- package/dist/{chunk-3JOLELB4.js → chunk-SJDFAK5R.js} +23 -5
- package/dist/chunk-SJDFAK5R.js.map +1 -0
- package/dist/{index-bfbWLC7_.d.cts → index-CZylXAP4.d.cts} +278 -2
- package/dist/{index-C31E7M9l.d.ts → index-DwkSyAip.d.ts} +278 -2
- package/dist/index.cjs +400 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -79
- package/dist/index.d.ts +6 -79
- package/dist/index.js +21 -204
- package/dist/index.js.map +1 -1
- package/dist/native.cjs +589 -4
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +1 -1
- package/dist/native.d.ts +1 -1
- package/dist/native.js +2 -2
- package/eslint/index.cjs +172 -0
- package/eslint/index.d.ts +6 -0
- package/eslint/index.mjs +5 -0
- package/package.json +17 -2
- package/dist/chunk-3JOLELB4.js.map +0 -1
- package/dist/chunk-PRZP2SB6.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## 0.89.0
|
|
2
|
+
|
|
3
|
+
Card follows the theme, and the form set gains Checkbox and FileInput. Follow-up to the 0.88.0 migrations.
|
|
4
|
+
|
|
5
|
+
- **Card tokens.** Surface, border and text come from new `--app-card-bg`, `--app-card-border`, `--app-card-fg` and `--app-card-fg-muted` (chrome.css). Light: `#FFFFFF`, `#D7DADF`, `#2F3136`, `#5C6B80`. Dark (under `.dark`): `#161616`, `#333333`, `#E8E8E8`, `#A3A3A3`. Both pairs clear 4.5:1. This fixes the white cards on surveys dark mode and the petitions contrast failure on dark pages. The Brand `.dark {}` can still override the tokens.
|
|
6
|
+
- **Card props.** `subtitle`, `aside`, `actions`, `footer`, `bodyClassName`, `padding` (`none | sm | md | lg`, default `md`). A plain `<Card title>` renders as before (p-6, h3 title). Any of `subtitle`, `aside`, `actions`, `footer`, `bodyClassName` switches to a bordered header / body / footer layout, which is what dashboard `RdParts` and settings `ui/kit` Card need.
|
|
7
|
+
- **New `Checkbox`.** `label`, `description`, `indeterminate`, `invalid`, forwards its ref, 44px-tall clickable row, accent fill, `data-shell-field`. Exported from the root and `/native`.
|
|
8
|
+
- **New `FileInput`.** The `type="file"` control with the Input label/helper/error chrome, 44px tall, secondary-button styling on the native button. `Input` still rejects `file` and `checkbox`; its docs point to these.
|
|
9
|
+
- **ESLint guard.** `no-raw-control-styling` now flags `<input type="checkbox">` and `<input type="file">` with `className` or `style` unless the file imports `Checkbox` / `FileInput` (importing `Input` no longer excuses them). `no-local-primitives` also covers `Checkbox` and `FileInput`.
|
|
10
|
+
- Tests: `__tests__/cardCheckboxFileInput.test.tsx` (including light and dark contrast of the card tokens), `__tests__/eslintGuard.test.ts`.
|
|
11
|
+
|
|
12
|
+
## 0.88.0
|
|
13
|
+
|
|
14
|
+
Shared form primitives, so apps stop hand-rolling them.
|
|
15
|
+
|
|
16
|
+
- **New exports** `Button`, `Input`, `Select`, `Textarea` (plus `ButtonProps`, `ButtonVariant`, `ButtonSize`, `InputProps`, `SelectProps`, `TextareaProps`).
|
|
17
|
+
- **Button.** Variants `primary | secondary | ghost | danger`; sizes `md` (44px) and `sm` (36px); `loading` and `disabled`; `href` / `as` link-buttons; forwards its ref. Primary uses the `--app-btn-primary-*` tokens (Specs buttonPrimary).
|
|
18
|
+
- **Input, Select, Textarea.** `label`, `helperText`, `error`, `invalid`; 44px control height for Input and Select; forward their refs; `--app-field-*` tokens.
|
|
19
|
+
- **Rule.** Apps use these instead of a local Button / Input / Select / Textarea (README "Form primitives").
|
|
20
|
+
- **Native subpath.** `Button`, `Input`, `Select`, `Textarea`, `Card`, `PageHeader`, `FormColumn`, `BreadcrumbBar`, `HeaderTabBar` (and their types) are also exported from `@the-portland-company/shell/native`, which has no Chakra or react-router peer. The native `PageHeader` and `BreadcrumbBar` render breadcrumb links as plain anchors. The main entry is unchanged.
|
|
21
|
+
- Tests: `__tests__/formPrimitives.test.tsx`.
|
|
22
|
+
|
|
1
23
|
## 0.87.1
|
|
2
24
|
|
|
3
25
|
Release fix only; same code as 0.87.0, which never reached npm. The 0.87.0 version bump regenerated `package-lock.json` with a newer npm that dropped two optional `yaml@2.9.1` entries, so the publish gate's `npm ci` refused it. The lock file is restored from 0.86.0 with only its version changed.
|
package/README.md
CHANGED
|
@@ -171,6 +171,57 @@ All `<ShellChrome>` props are optional — only supply what your app has. The ch
|
|
|
171
171
|
|
|
172
172
|
---
|
|
173
173
|
|
|
174
|
+
## Form primitives
|
|
175
|
+
|
|
176
|
+
Shared `Button`, `Input`, `Select`, `Textarea`, `Checkbox`, `FileInput` and `Card`, styled from the shell's tokens (`--app-btn-*`, `--app-field-*`, `--color-*`). 5px radius, 12px horizontal padding.
|
|
177
|
+
|
|
178
|
+
| Export | Props |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `<Button>` | `variant`: `primary` (default) \| `secondary` \| `ghost` \| `danger`; `size`: `md` (44px, default) \| `sm` (36px); `loading` (spinner, `aria-busy`, blocks clicks); `disabled`; `href` or `as` for a link-button (e.g. `as={Link} to="/x"`); `type` defaults to `button`; forwards its ref. |
|
|
181
|
+
| `<Input>` | All native input props plus `label`, `helperText`, `error`, `invalid`, `wrapperClassName`. 44px tall. Forwards its ref to the `<input>`. |
|
|
182
|
+
| `<Select>` | Same as `Input`, with `<option>` children. 44px tall. Forwards its ref to the `<select>`. |
|
|
183
|
+
| `<Textarea>` | Same as `Input`, 88px minimum height. Forwards its ref to the `<textarea>`. |
|
|
184
|
+
| `<Checkbox>` | All native checkbox props plus `label`, `description`, `indeterminate`, `invalid`, `wrapperClassName`. The whole row is a 44px touch target; accent fill; `data-shell-field`. Forwards its ref to the `<input>`. |
|
|
185
|
+
| `<FileInput>` | The `type="file"` control: `label`, `helperText`, `error`, `invalid`, `accept`, `multiple`, `wrapperClassName`. 44px tall. Forwards its ref. `Input` does not accept `type="file"` or `"checkbox"`. |
|
|
186
|
+
| `<Card>` | `title`, `subtitle`, `aside`, `actions`, `footer`, `bodyClassName`, `padding` (`none \| sm \| md \| lg`, default `md`), `minH`, plus any div props. Plain `<Card title>` is a padded box with an h3; any of `subtitle`, `aside`, `actions`, `footer`, `bodyClassName` gives a bordered header / body / footer. |
|
|
187
|
+
|
|
188
|
+
**Card and dark mode.** Card and Checkbox read `--app-card-bg`, `--app-card-border`, `--app-card-fg` and `--app-card-fg-muted` from `chrome.css`: light on `:root`, dark under `.dark`. Do not paint a card white in app CSS; override these tokens if an app needs a different surface.
|
|
189
|
+
|
|
190
|
+
An `error` message marks the control invalid (`aria-invalid`, red border) and replaces the helper text; the label, helper and error are wired with `htmlFor` and `aria-describedby`.
|
|
191
|
+
|
|
192
|
+
These primitives (and `Card`, `PageHeader`, `FormColumn`) are also exported from `@the-portland-company/shell/native`, which needs no Chakra or react-router peer. Apps without those peers should import from `native`.
|
|
193
|
+
|
|
194
|
+
**Rule:** apps must use these instead of a local `Button`, `Input`, `Select` or `Textarea`. Do not hand-roll form controls or button styles in an app; if a control needs a new variant or state, add it here so every app gets it.
|
|
195
|
+
|
|
196
|
+
## Enforcement
|
|
197
|
+
|
|
198
|
+
Spec: Components — shared primitives are mandatory. Apps must not hand-roll Button/Card/Input/Select/Textarea, and must not fall behind the shell version. Both are checked mechanically.
|
|
199
|
+
|
|
200
|
+
Adopt in three lines:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
// eslint.config.js (ESLint 9+ flat config)
|
|
204
|
+
import shell from '@the-portland-company/shell/eslint'
|
|
205
|
+
export default [...shell.configs.recommended /* , your other configs */]
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
// package.json
|
|
210
|
+
{ "scripts": { "lint": "eslint . && shell-version-check" } }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Run `shell-version-check` in CI too (it is a package `bin`, so `npx shell-version-check` works after install).
|
|
214
|
+
|
|
215
|
+
| Rule (`politogy-shell/…`) | Fails on |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `no-local-primitives` | Declaring or exporting a local `Button`, `Card`, `Input`, `Select`, `Textarea`, `Checkbox`, `FileInput`, `PageHeader` or `FormColumn` (re-exports from the shell are fine). |
|
|
218
|
+
| `no-raw-control-styling` | JSX `<button>`, `<input>`, `<select>`, `<textarea>` with `className` or `style`. Allowed in a file that imports the shell primitive of the same kind (wrapping). `<input type="checkbox">` needs `Checkbox` and `<input type="file">` needs `FileInput`; importing `Input` does not excuse them. |
|
|
219
|
+
| `no-global-control-classes` | `className` strings matching `\b(btn\|input)(-[a-z]+)?\b`, including inside `clsx(...)`, ternaries and template literals. |
|
|
220
|
+
|
|
221
|
+
Escape hatch: put `// shell-primitive-exempt: <reason>` on the line above (or the same line as) the offending code. A reason is required. Prefer adding the missing variant to the shell instead.
|
|
222
|
+
|
|
223
|
+
`shell-version-check` exits 1 when the app's declared shell range cannot install the latest published version (`npm view`, 10s timeout; exit 2 if that lookup fails). `--offline` compares against the installed shell version instead; `--cwd=<dir>` checks another package. The shell package itself does not enable these rules.
|
|
224
|
+
|
|
174
225
|
## CSS imports
|
|
175
226
|
|
|
176
227
|
```tsx
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fails when the app's declared @the-portland-company/shell range cannot
|
|
3
|
+
// satisfy the latest published minor (or, with --offline, the installed one).
|
|
4
|
+
// Exit: 0 ok, 1 behind, 2 could not determine.
|
|
5
|
+
import { readFileSync, existsSync, realpathSync } from 'node:fs'
|
|
6
|
+
import { execFileSync } from 'node:child_process'
|
|
7
|
+
import { resolve, join } from 'node:path'
|
|
8
|
+
import { pathToFileURL } from 'node:url'
|
|
9
|
+
|
|
10
|
+
export const PKG = '@the-portland-company/shell'
|
|
11
|
+
|
|
12
|
+
const V = /^v?(\d+)\.(\d+)\.(\d+)/
|
|
13
|
+
export function parseVersion(s) {
|
|
14
|
+
const m = V.exec(String(s).trim())
|
|
15
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null
|
|
16
|
+
}
|
|
17
|
+
const cmp = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]
|
|
18
|
+
|
|
19
|
+
// One comparator token -> predicate list. Supports ^ ~ >= > <= < = x-ranges and *.
|
|
20
|
+
function comparator(tok) {
|
|
21
|
+
tok = tok.trim()
|
|
22
|
+
if (tok === '' || tok === '*' || tok === 'x' || tok === 'latest') return [() => true]
|
|
23
|
+
const m = /^(\^|~|>=|<=|>|<|=)?\s*v?(\d+|x|\*)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?(?:-[\w.]+)?$/.exec(tok)
|
|
24
|
+
if (!m) throw new Error(`Unsupported range "${tok}"`)
|
|
25
|
+
const op = m[1] || ''
|
|
26
|
+
const wild = (x) => x === undefined || x === 'x' || x === '*'
|
|
27
|
+
const p = [m[2], m[3], m[4]].map((x) => (wild(x) ? null : Number(x)))
|
|
28
|
+
const lo = [p[0] ?? 0, p[1] ?? 0, p[2] ?? 0]
|
|
29
|
+
const ge = (v) => cmp(v, lo) >= 0
|
|
30
|
+
if (p[0] === null) return [() => true]
|
|
31
|
+
if (op === '>=') return [ge]
|
|
32
|
+
if (op === '>') return [(v) => (p[1] === null ? v[0] > p[0] : p[2] === null ? v[0] > p[0] || (v[0] === p[0] && v[1] > p[1]) : cmp(v, lo) > 0)]
|
|
33
|
+
if (op === '<') return [(v) => cmp(v, lo) < 0]
|
|
34
|
+
if (op === '<=') return [(v) => (p[1] === null ? v[0] <= p[0] : p[2] === null ? v[0] < p[0] || (v[0] === p[0] && v[1] <= p[1]) : cmp(v, lo) <= 0)]
|
|
35
|
+
let upper
|
|
36
|
+
if (op === '^') {
|
|
37
|
+
upper = p[0] > 0 || p[1] === null ? [p[0] + 1, 0, 0] : p[1] > 0 || p[2] === null ? [0, p[1] + 1, 0] : [0, 0, p[2] + 1]
|
|
38
|
+
} else if (op === '~') {
|
|
39
|
+
upper = p[1] === null ? [p[0] + 1, 0, 0] : [p[0], p[1] + 1, 0]
|
|
40
|
+
} else {
|
|
41
|
+
// exact or x-range
|
|
42
|
+
upper = p[1] === null ? [p[0] + 1, 0, 0] : p[2] === null ? [p[0], p[1] + 1, 0] : [p[0], p[1], p[2] + 1]
|
|
43
|
+
}
|
|
44
|
+
return [ge, (v) => cmp(v, upper) < 0]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function satisfies(version, range) {
|
|
48
|
+
const v = parseVersion(version)
|
|
49
|
+
if (!v) throw new Error(`Bad version "${version}"`)
|
|
50
|
+
const r = String(range).trim()
|
|
51
|
+
if (/^(npm:|file:|link:|git|https?:|workspace:)/.test(r)) throw new Error(`Cannot evaluate range "${r}"`)
|
|
52
|
+
return r.split('||').some((set) => {
|
|
53
|
+
const toks = set.trim().replace(/(>=|<=|>|<|=|\^|~)\s+/g, '$1').split(/\s+/)
|
|
54
|
+
return toks.every((t) => comparator(t).every((f) => f(v)))
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function declaredRange(pkgJson) {
|
|
59
|
+
for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
60
|
+
if (pkgJson[k] && pkgJson[k][PKG]) return pkgJson[k][PKG]
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Pure decision: returns { ok, message }. */
|
|
66
|
+
export function evaluate({ range, target, source }) {
|
|
67
|
+
if (satisfies(target, range)) {
|
|
68
|
+
return { ok: true, message: `ok: ${PKG} "${range}" accepts ${source} ${target}.` }
|
|
69
|
+
}
|
|
70
|
+
const [maj, min] = parseVersion(target)
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
message:
|
|
74
|
+
`FAIL: ${PKG} is declared as "${range}", which cannot install ${source} ${target}.\n` +
|
|
75
|
+
`Upgrade: npm install ${PKG}@^${maj}.${min}.0 (apps must not fall behind the shell).`,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function main(argv) {
|
|
80
|
+
const offline = argv.includes('--offline')
|
|
81
|
+
const cwdArg = argv.find((a) => a.startsWith('--cwd='))
|
|
82
|
+
const cwd = resolve(cwdArg ? cwdArg.slice(6) : process.cwd())
|
|
83
|
+
const pj = join(cwd, 'package.json')
|
|
84
|
+
if (!existsSync(pj)) {
|
|
85
|
+
console.error(`shell-version-check: no package.json in ${cwd}`)
|
|
86
|
+
return 2
|
|
87
|
+
}
|
|
88
|
+
const range = declaredRange(JSON.parse(readFileSync(pj, 'utf8')))
|
|
89
|
+
if (!range) {
|
|
90
|
+
console.error(`shell-version-check: ${PKG} is not a dependency of this package.`)
|
|
91
|
+
return 2
|
|
92
|
+
}
|
|
93
|
+
let target, source
|
|
94
|
+
try {
|
|
95
|
+
if (offline) {
|
|
96
|
+
const ip = join(cwd, 'node_modules', ...PKG.split('/'), 'package.json')
|
|
97
|
+
if (!existsSync(ip)) throw new Error(`${PKG} is not installed (run npm install)`)
|
|
98
|
+
target = JSON.parse(readFileSync(ip, 'utf8')).version
|
|
99
|
+
source = 'installed'
|
|
100
|
+
} else {
|
|
101
|
+
const out = execFileSync('npm', ['view', PKG, 'version'], { encoding: 'utf8', timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
102
|
+
target = out.trim()
|
|
103
|
+
source = 'latest published'
|
|
104
|
+
}
|
|
105
|
+
const res = evaluate({ range, target, source })
|
|
106
|
+
;(res.ok ? console.log : console.error)(res.message)
|
|
107
|
+
return res.ok ? 0 : 1
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error(`shell-version-check: could not determine version (${e.message.split('\n')[0]}). Use --offline to compare against the installed shell.`)
|
|
110
|
+
return 2
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
115
|
+
process.exit(main(process.argv.slice(2)))
|
|
116
|
+
}
|
|
117
|
+
export { main }
|