mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-08 01:53:30 +01:00
feat(styles): STYLE-005 smart style suggestion engine
- StyleAnalyzer: scans project for repeated raw colors/spacing (3+ occurrences) and nodes with 3+ non-token overrides (variant candidates) - StyleAnalyzer.toSuggestions() converts analysis to ordered StyleSuggestion[] - StyleAnalyzer.analyzeNode() for per-node suggestions from property panel - SuggestionActionHandler: creates tokens from repeated values + replaces all occurrences; sets _variant param for variant candidates - SuggestionBanner: compact inline UI component with accept/dismiss/never - useStyleSuggestions: hook with localStorage persist for dismissed state - TokenModelLike interface keeps analyzer decoupled from StyleTokensModel singleton - StyleAnalysisOptions allows injecting tokenModel for existing-token matching
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
.Banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
padding: var(--spacing-sm, 8px) var(--spacing-md, 12px);
|
||||
background-color: var(--theme-color-bg-3, #2a2a2d);
|
||||
border: 1px solid var(--theme-color-border-default, #3d3d40);
|
||||
border-left: 3px solid var(--theme-color-primary, #e03b3b);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
margin-bottom: var(--spacing-sm, 8px);
|
||||
}
|
||||
|
||||
.Indicator {
|
||||
flex-shrink: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--theme-color-primary, #e03b3b);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.Body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.Message {
|
||||
margin: 0 0 6px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: var(--theme-color-fg-default, #d4d4d8);
|
||||
}
|
||||
|
||||
.Actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs, 4px);
|
||||
}
|
||||
|
||||
.AcceptButton {
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--theme-color-fg-highlight, #ffffff);
|
||||
background-color: var(--theme-color-primary, #e03b3b);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm, 3px);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.DismissButton {
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
color: var(--theme-color-fg-default-shy, #a1a1aa);
|
||||
background: transparent;
|
||||
border: 1px solid var(--theme-color-border-default, #3d3d40);
|
||||
border-radius: var(--radius-sm, 3px);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-color-fg-default, #d4d4d8);
|
||||
}
|
||||
}
|
||||
|
||||
.CloseButton {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
color: var(--theme-color-fg-default-shy, #a1a1aa);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-top: 1px;
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-color-fg-default, #d4d4d8);
|
||||
background-color: var(--theme-color-bg-2, #1c1c1e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* STYLE-005: SuggestionBanner
|
||||
*
|
||||
* Non-intrusive inline banner shown in the property panel when a style
|
||||
* suggestion is available. Intentionally minimal — no emojis, no clutter.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import css from './SuggestionBanner.module.scss';
|
||||
|
||||
export interface SuggestionBannerSuggestion {
|
||||
id: string;
|
||||
message: string;
|
||||
acceptLabel: string;
|
||||
}
|
||||
|
||||
export interface SuggestionBannerProps {
|
||||
suggestion: SuggestionBannerSuggestion;
|
||||
/** Called when the user clicks the primary action button. */
|
||||
onAccept: () => void;
|
||||
/** Called when the user clicks Ignore (dismisses for the current session). */
|
||||
onDismiss: () => void;
|
||||
/** Called when the user clicks the × (persists dismiss forever for this type). */
|
||||
onNeverShow: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single style suggestion as a compact banner.
|
||||
*
|
||||
* @example
|
||||
* <SuggestionBanner
|
||||
* suggestion={activeSuggestion}
|
||||
* onAccept={handleAccept}
|
||||
* onDismiss={handleDismiss}
|
||||
* onNeverShow={handleNeverShow}
|
||||
* />
|
||||
*/
|
||||
export function SuggestionBanner({ suggestion, onAccept, onDismiss, onNeverShow }: SuggestionBannerProps) {
|
||||
return (
|
||||
<div className={css.Banner} role="region" aria-label="Style suggestion">
|
||||
<div className={css.Indicator} aria-hidden="true" />
|
||||
|
||||
<div className={css.Body}>
|
||||
<p className={css.Message}>{suggestion.message}</p>
|
||||
|
||||
<div className={css.Actions}>
|
||||
<button type="button" className={css.AcceptButton} onClick={onAccept}>
|
||||
{suggestion.acceptLabel}
|
||||
</button>
|
||||
|
||||
<button type="button" className={css.DismissButton} onClick={onDismiss}>
|
||||
Ignore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={css.CloseButton}
|
||||
onClick={onNeverShow}
|
||||
aria-label="Never show this suggestion type"
|
||||
title="Don't suggest this again"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { SuggestionBanner } from './SuggestionBanner';
|
||||
export type { SuggestionBannerProps, SuggestionBannerSuggestion } from './SuggestionBanner';
|
||||
Reference in New Issue
Block a user