Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 73 additions & 9 deletions .github/workflows/weekly-api-diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Build current API snapshot
run: yarn build:api-branch

# Build release baseline using the last Publish commit (~2 min, always fresh)
# Build release baseline using the last minor/major release commit (skips patch-only releases)
- name: Build release baseline
run: yarn build:api-published

Expand All @@ -50,38 +50,90 @@ jobs:
CURRENT_PUBLISH=$(git log --grep='^Publish$' --oneline -1 | awk '{print $1}')
PREV_PUBLISH=$(cat snapshots/last-publish-hash.txt 2>/dev/null || echo "")

echo "=== Release detection ==="
echo "CURRENT_PUBLISH=$CURRENT_PUBLISH"
echo "PREV_PUBLISH=$PREV_PUBLISH"

if [ -n "$PREV_PUBLISH" ] && [ "$CURRENT_PUBLISH" != "$PREV_PUBLISH" ]; then
# New release landed so skip comparing to last week, commit fresh as new baseline
echo "NEW_RELEASE=true" >> $GITHUB_ENV
# Detect minor/major vs patch: check if s2 or react-aria-components got a x.y.0 tag on this Publish commit
IS_MINOR=$(git tag --points-at "$CURRENT_PUBLISH" | grep -E '^(@react-spectrum/s2|react-aria-components)@[0-9]+\.[0-9]+\.0$' | head -1)
echo "IS_MINOR_OR_MAJOR=${IS_MINOR:-(none)}"
if [ -n "$IS_MINOR" ]; then
echo "Minor/major release detected, resetting baseline"
echo "NEW_RELEASE=true" >> $GITHUB_ENV
else
echo "Patch release detected, updating hash, continuing with delta"
echo "$CURRENT_PUBLISH" > snapshots/last-publish-hash.txt
fi
else
echo "No new release"
fi

NEW_RELEASE="${NEW_RELEASE:-false}"

if [ "$NEW_RELEASE" != "true" ]; then
# Compare against the last diff in the snapshots repo
PREV=$(ls snapshots/diffs/*.txt 2>/dev/null | sort -r | head -1)
echo ""
echo "=== Delta computation ==="
echo "Comparing against: ${PREV:-(none, first run)}"
if [ -n "$PREV" ]; then
diff "$PREV" /tmp/diff-current.txt > /tmp/weekly-delta.txt || true
else
echo "(first run — no previous diff to compare against)" > /tmp/weekly-delta.txt
fi
fi

# Commit a diff if there is a new release (fresh baseline), or diff changed from last week
echo ""
echo "=== File sizes ==="
echo "diff-current.txt: $(wc -c < /tmp/diff-current.txt) bytes"
ls /tmp/weekly-delta.txt 2>/dev/null && echo "weekly-delta.txt: $(wc -c < /tmp/weekly-delta.txt) bytes" || echo "weekly-delta.txt: not created"
echo ""
echo "=== Delta content (first 20 lines) ==="
head -20 /tmp/weekly-delta.txt 2>/dev/null || echo "(none)"

# Commit a diff if there is a new minor/major release (fresh baseline), or diff changed from last week
# Skip if no difference from last diff (aka no change from last week/last run), or if the diff against the release code is empty
NEW_RELEASE="${NEW_RELEASE:-false}"
echo ""
echo "=== Commit decision ==="
if [ -s /tmp/diff-current.txt ] && ([ "$NEW_RELEASE" = "true" ] || [ -s /tmp/weekly-delta.txt ]); then
echo "Committing diff to snapshots repo"
cp /tmp/diff-current.txt snapshots/diffs/$TODAY.txt
mkdir -p snapshots/deltas
if [ -s /tmp/weekly-delta.txt ]; then
# Save processed delta: strip > / < markers and line-number markers,
# split into two labeled sections so it can be passed directly to the model
{
echo "=== NEW API CHANGES THIS WEEK (not in last week's diff) ==="
grep '^> ' /tmp/weekly-delta.txt | sed 's/^> //'
RELEASED=$(grep '^< ' /tmp/weekly-delta.txt | sed 's/^< //')
if [ -n "$RELEASED" ]; then
echo ""
echo "=== API CHANGES RELEASED SINCE LAST WEEK (were in last week's diff, now gone) ==="
echo "$RELEASED"
fi
} > snapshots/deltas/$TODAY.txt
fi
cd snapshots
git config user.email "github-actions@github.com"
git config user.name "GitHub Actions"
git add diffs/$TODAY.txt
[ -f deltas/$TODAY.txt ] && git add deltas/$TODAY.txt
echo "$CURRENT_PUBLISH" > last-publish-hash.txt
git add last-publish-hash.txt
echo "=== Staged changes ==="
git diff --cached --stat
git diff --cached --quiet || (git commit -m "weekly api diff $TODAY" && git push)
else
echo "Skipping commit — diff-current empty=$([ ! -s /tmp/diff-current.txt ] && echo yes || echo no), new_release=$NEW_RELEASE, delta_empty=$([ ! -s /tmp/weekly-delta.txt ] && echo yes || echo no)"
fi

# Summarize with GitHub Models (free via GITHUB_TOKEN) and post to Slack
- name: Summarize and post to Slack
env:
SLACK_TSDIFF_CHROMATIC_BOT_TOKEN: ${{ secrets.SLACK_TSDIFF_CHROMATIC_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
TEST_SLACK_ID: ${{ secrets.TEST_SLACK_ID }}
GITHUB_TOKEN: ${{ github.token }}
run: |
python3 << 'PYEOF'
Expand All @@ -99,6 +151,7 @@ jobs:
github_token = os.environ['GITHUB_TOKEN']
workspace = os.environ['GITHUB_WORKSPACE']
diff_url = f"https://github.com/{snapshots_repo}/blob/main/diffs/{today}.txt"
delta_url = f"https://github.com/{snapshots_repo}/blob/main/deltas/{today}.txt"

vs_release_size = os.path.getsize('/tmp/diff-current.txt')
vs_last_week_size = os.path.getsize('/tmp/weekly-delta.txt') if os.path.exists('/tmp/weekly-delta.txt') else 0
Expand All @@ -117,7 +170,9 @@ jobs:
elif new_release:
message = f"📊 Weekly API Diff — {today}\n\nNew release since last diff — resetting baseline. Full diff vs release: {diff_url}\n\nReact ✅ if changes look expected, or 🚨 if something looks wrong."
else:
delta = open('/tmp/weekly-delta.txt').read()[:4000]
# Read the already-processed delta saved by the shell step
delta_path = f"{workspace}/snapshots/deltas/{today}.txt"
model_input = open(delta_path).read() if os.path.exists(delta_path) else ""

# Extract classification rules from prompt.md (single source of truth)
prompt_md = open(f"{workspace}/scripts/weekly-api-diff/prompt.md").read()
Expand All @@ -127,10 +182,19 @@ jobs:

payload = {
"model": "gpt-4o-mini",
"max_tokens": 600,
"max_tokens": 800,
"messages": [{
"role": "user",
"content": f"Summarize this week-to-week react-spectrum API diff in under 200 words using bullet points.\n\n{rules}\n\nDelta (changes from last week):\n{delta}"
"content": (
f"Summarize this week's react-spectrum API changes using grouped bullet points. Group related changes together (e.g. multiple layout classes losing the same method = one bullet). For each group or item:\n"
f"- Name the component(s) or interface(s)\n"
f"- Say what changed (added, removed, signature changed)\n"
f"- Flag net-new components \n"
f"Don't spell out full type signatures. Aim for a knowledgeable teammate skimming Slack.\n\n"
f"IMPORTANT: Only report what is explicitly listed below. Do not infer or add anything from your training knowledge.\n\n"
f"{rules}\n\n"
f"{model_input}"
)
}]
}

Expand All @@ -143,7 +207,7 @@ jobs:
}
)
summary = json.loads(urllib.request.urlopen(req).read())['choices'][0]['message']['content']
message = f"📊 Weekly API Diff — {today}\n\n{summary}\n\nFull diff vs release: {diff_url}\n\nReact ✅ if changes look expected, or 🚨 if something looks wrong."
message = f"📊 Weekly API Diff — {today}\n\n{summary}\n\nWhat's new this week: {delta_url}\nFull diff vs release: {diff_url}\n\nReact ✅ if changes look expected, or 🚨 if something looks wrong."

req = urllib.request.Request(
'https://slack.com/api/chat.postMessage',
Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
"editor.formatOnSaveMode": "file",
"typescript.experimental.useTsgo": true
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"release": "lerna publish from-package --yes",
"version:nightly": "yarn workspaces foreach --all --no-private -t version -d 3.0.0-nightly-$(git rev-parse --short HEAD)-$(date +'%y%m%d') && yarn apply-nightly --all",
"publish:nightly": "yarn workspaces foreach --all --no-private -t npm publish --tag nightly --access public",
"build:api-published": "node scripts/buildBranchAPI.js --githash=$(git log --grep='^Publish$' --oneline -1 | awk '{print $1}') --output=base-api",
"build:api-published": "node scripts/buildBranchAPI.js --githash=$(git rev-list -n 1 $(git tag -l 'react-aria-components@*' | grep -E '@[0-9]+\\.[0-9]+\\.0$' | sort -V | tail -1)) --output=base-api",
"build:api-branch": "node scripts/buildBranchAPI.js",
"compare:apis": "node scripts/compareAPIs.js",
"check-apis": "yarn build:api-branch --githash=\"origin/main\" --output=\"base-api\" && yarn build:api-branch && yarn compare:apis",
Expand Down
9 changes: 9 additions & 0 deletions packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import {ColorField, ColorFieldProps} from '../src/ColorField';
import {ColorSwatch} from '../exports';
import {generateComboChunks, shortName} from './utils';
import type {Meta, StoryObj} from '@storybook/react';
import {ReactElement} from 'react';
Expand Down Expand Up @@ -144,3 +145,11 @@ export const LabelPositionTopPt5: StoryObj<typeof Template> = {
};

// Skipped the contextual help stories from here on out since its all shared

export const ExampleWithPrefix: StoryObj<typeof Template> = {
render: args => <ColorField {...args} value="#ff00ff" />,
args: {
label: 'Color',
prefix: <ColorSwatch size="XS" color="#ff00ff" />
}
};
8 changes: 8 additions & 0 deletions packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
WithAvatars,
WithIcons
} from '../stories/ComboBox.stories';
import {Avatar} from '../exports';
import {ComboBox} from '../src/ComboBox';
import {expect} from '@storybook/jest';
import type {Meta, StoryObj} from '@storybook/react';
Expand Down Expand Up @@ -192,3 +193,10 @@ export const Filtering: StoryObj<AsyncComboBoxStoryType> = {
);
}
};

export const WithPrefix: Story = {
...Example,
args: {
prefix: <Avatar size={20} src="https://i.imgur.com/xIe7Wlb.png" />
}
};
10 changes: 10 additions & 0 deletions packages/@react-spectrum/s2/chromatic/NumberField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,13 @@ export const ContextualHelpExample: Story = {
)
}
};

export const ExampleWithPrefix: Story = {
render: args => <NumberField {...args} />,
args: {
label: 'Price',
prefix: 'USD',
formatOptions: {style: 'currency', currency: 'USD', currencyDisplay: 'narrowSymbol'},
placeholder: '0.00'
}
};
6 changes: 5 additions & 1 deletion packages/@react-spectrum/s2/src/CenterBaseline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@

import {css} from '../style/style-macro' with {type: 'macro'};
import {CSSProperties, ReactNode} from 'react';
import {DOMAttributes} from '@react-types/shared';
import {filterDOMProps} from 'react-aria/filterDOMProps';
import {mergeStyles} from '../style/runtime';
import {style} from '../style' with {type: 'macro'};
import {StyleString} from '../style/types';

interface CenterBaselineProps {
interface CenterBaselineProps extends DOMAttributes {
style?: CSSProperties;
styles?: StyleString;
children: ReactNode;
Expand All @@ -29,8 +31,10 @@ const styles = style({
});

export function CenterBaseline(props: CenterBaselineProps): ReactNode {
let domProps = filterDOMProps(props);
return (
<div
{...domProps}
slot={props.slot}
style={props.style}
className={mergeStyles(styles, props.styles) + ' ' + centerBaselineBefore}>
Expand Down
33 changes: 28 additions & 5 deletions packages/@react-spectrum/s2/src/ColorField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,23 @@ import {
ColorField as AriaColorField,
ColorFieldProps as AriaColorFieldProps
} from 'react-aria-components/ColorField';

import {ContextValue} from 'react-aria-components/slots';
import {createContext, forwardRef, Ref, useContext, useImperativeHandle, useRef} from 'react';
import {
createContext,
forwardRef,
ReactNode,
Ref,
useContext,
useImperativeHandle,
useRef
} from 'react';
import {createFocusableRef} from './useDOMRef';
import {field, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText, Input} from './Field';
import {FormContext, useFormProps} from './Form';
import {GlobalDOMAttributes, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared';
import {InputProps} from 'react-aria-components/Input';
import {InputContext, InputProps} from 'react-aria-components/Input';
import {mergeRefs} from 'react-aria/mergeRefs';
import {style} from '../style' with {type: 'macro'};
import {TextFieldRef} from './TextField';
import {useSpectrumContextProps} from './useSpectrumContextProps';
Expand All @@ -43,6 +51,11 @@ export interface ColorFieldProps
* @default 'M'
*/
size?: 'S' | 'M' | 'L' | 'XL';
/**
* The prefix to display in the ColorField. A non-interactive element that appears before the
* input.
*/
prefix?: ReactNode;
}

export const ColorFieldContext =
Expand Down Expand Up @@ -114,8 +127,18 @@ export const ColorField = forwardRef(function ColorField(
contextualHelp={props.contextualHelp}>
{label}
</FieldLabel>
<FieldGroup size={props.size}>
<Input ref={inputRef} />
<FieldGroup prefix={props.prefix} size={props.size}>
<InputContext.Consumer>
{ctx => (
<InputContext.Provider
value={{
...ctx,
ref: mergeRefs((ctx as any)?.ref, inputRef)
}}>
<Input />
</InputContext.Provider>
)}
</InputContext.Consumer>
{isInvalid && <FieldErrorIcon isDisabled={isDisabled} />}
</FieldGroup>
<HelpText
Expand Down
11 changes: 10 additions & 1 deletion packages/@react-spectrum/s2/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export interface ComboboxStyleProps {
* @default 'M'
*/
size?: 'S' | 'M' | 'L' | 'XL';
/**
* The prefix to display in the ComboBox. A non-interactive element that appears before the input.
*/
prefix?: ReactNode;
}
export interface ComboBoxProps<T extends object>
extends
Expand Down Expand Up @@ -702,6 +706,7 @@ const ComboboxInner = forwardRef(function ComboboxInner(
{label}
</FieldLabel>
<FieldGroup
prefix={props.prefix}
role="presentation"
isDisabled={isDisabled}
isInvalid={isInvalid}
Expand All @@ -715,7 +720,11 @@ const ComboboxInner = forwardRef(function ComboboxInner(
})({size})}>
<InputContext.Consumer>
{ctx => (
<InputContext.Provider value={{...ctx, ref: mergeRefs((ctx as any)?.ref, inputRef)}}>
<InputContext.Provider
value={{
...ctx,
ref: mergeRefs((ctx as any)?.ref, inputRef)
}}>
<Input aria-describedby={spinnerId} />
</InputContext.Provider>
)}
Expand Down
Loading
Loading