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
21 changes: 15 additions & 6 deletions .github/workflows/version-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ name: Sync version artifacts
# Triggered whenever a GitHub release or pre-release is published.
# Checks out main, aligns all non-JS version references to the release tag,
# rebuilds CSS bundles, publishes them to the dist branch, captures the
# resulting commit SHA, stores it in SLASHED_BRICKS_DIST_SHA, and commits
# resulting commit SHA, stores it in SLASHED_DIST_SHA (slashed.php),
# SLASHED_BRICKS_DIST_SHA (slashed-bricks.php), and
# SLASHED_GUTENBERG_DIST_SHA (slashed-gutenberg.php), then commits
# everything back to main.
#
# CSS bundles are NOT committed to the main branch — they live only on the
# dedicated `dist` branch and in GitHub Release assets. The dist-branch
# commit SHA is stored in slashed-bricks.php so the plugin can construct an
# immutable jsDelivr CDN URL that never changes between releases.
# commit SHA is stored in each integration entry point so the plugin can
# construct an immutable jsDelivr CDN URL that never changes between releases.
#
# Full release pipeline:
# 1. Developer runs `npm run release` locally → bumps package.json, runs
Expand Down Expand Up @@ -106,12 +108,19 @@ jobs:
- name: Switch back to main working tree
run: git checkout main

- name: Update SLASHED_BRICKS_DIST_SHA in PHP
- name: Update DIST_SHA constants in PHP
env:
DIST_SHA: ${{ steps.dist.outputs.sha }}
run: |
DIST_SHA="${{ steps.dist.outputs.sha }}"
sed -i \
"s|define( 'SLASHED_DIST_SHA', '[^']*' )|define( 'SLASHED_DIST_SHA', '${DIST_SHA}' )|" \
slashed.php
sed -i \
"s|define( 'SLASHED_BRICKS_DIST_SHA', '[^']*' )|define( 'SLASHED_BRICKS_DIST_SHA', '${DIST_SHA}' )|" \
integrations/bricks/slashed-bricks.php
sed -i \
"s|define( 'SLASHED_GUTENBERG_DIST_SHA', '[^']*' )|define( 'SLASHED_GUTENBERG_DIST_SHA', '${DIST_SHA}' )|" \
integrations/gutenberg/slashed-gutenberg.php
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Commit and push if anything changed
env:
Expand All @@ -120,7 +129,7 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Stage only tracked files (dist/*.css are gitignored).
git add integrations/bricks/slashed-bricks.php package.json package-lock.json
git add slashed.php integrations/bricks/slashed-bricks.php integrations/gutenberg/slashed-gutenberg.php package.json package-lock.json
git add -u
if git diff --cached --quiet; then
echo "Nothing to commit — all version artifacts already up to date."
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ semantic-review/
# Build artifacts in dist/ — generated by `npm run build`, served via the
# `dist` branch (version-sync.yml) and GitHub Releases. Not tracked in main
# to avoid binary conflicts and noise in PR diffs.
dist/slashed-bricks.zip
dist/slashed.zip
dist/*.css
dist/*.map
20 changes: 14 additions & 6 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,20 @@ stable and locked.

Ideas that are promising but not yet committed.

- **Gutenberg integration** — CSS loading + color palette sync + `theme.json`
custom property mapping. Scoped v1 is ~2–3 weeks of work and covers 80% of
what Gutenberg users need. Deferred because the `--sf-*` → `--wp--custom--*`
naming mismatch needs a clean translation layer, and Gutenberg's per-block
architecture is fundamentally different from Bricks' canvas model. reBEMer
parity is not a goal for any Gutenberg integration.
- **Gutenberg integration** *(in progress — `integrations/gutenberg/`)* — v1
ships as a standalone plugin (`slashed-gutenberg.php`) that can be activated
independently of the Bricks plugin. v1 scope: CSS bundle loading into the
block editor canvas + frontend, color palette sync (21 tokens — brand,
status, surface, text, border, link), and a `data-wp-dark-mode-active` bridge
so the editor dark-mode toggle drives SLASHED's color-scheme system.

Deliberately excluded from v1 (future work):
- `--sf-*` → `--wp--custom--*` theme.json mapping — only needed to expose
tokens in the Global Styles UI (Site Editor); not required for the 80% use
case. Analogy: ACSS does not do this either.
- Token override admin UI — no Svelte panel for Gutenberg yet; overrides via
the `slashed_gutenberg/css_bundle_url` filter or child-theme CSS.
- reBEMer parity — Bricks-specific; not applicable to the block editor.

- **Per-layer opt-in bundle via `@import`** — technically already possible
today using the granular `core/` and `optional/` source files (with
Expand Down
183 changes: 183 additions & 0 deletions includes/class-admin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php
/**
* Unified SLASHED admin page.
*
* @package SLASHED
*/

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Class Slashed_Admin
*
* Registers the top-level "SLASHED" menu and renders the integration
* toggle page. Builder-specific settings pages (e.g. Bricks token
* overrides) appear as sub-pages when their integration is active.
*/
class Slashed_Admin {

const PAGE_SLUG = 'slashed';
const NONCE_KEY = 'slashed_settings_save';
const NONCE_ACTION = 'slashed_save_settings';

public function __construct() {
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_action( 'admin_post_slashed_save_settings', array( $this, 'handle_save' ) );
}

public function register_menu() {
add_menu_page(
__( 'SLASHED', 'slashed' ),
__( 'SLASHED', 'slashed' ),
'manage_options',
self::PAGE_SLUG,
array( $this, 'render_page' ),
'dashicons-art',
59
);
}

/**
* Handle the settings form POST.
* Redirects back to the settings page with a status flag.
*/
public function handle_save() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'slashed' ) );
}

check_admin_referer( self::NONCE_ACTION, self::NONCE_KEY );

// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above via check_admin_referer.
$raw_integrations = isset( $_POST['integrations'] ) && is_array( $_POST['integrations'] )
? array_map( 'sanitize_key', $_POST['integrations'] )
: array();

// phpcs:ignore WordPress.Security.NonceVerification.Missing
$raw_bundle = isset( $_POST['css_bundle'] ) ? sanitize_key( $_POST['css_bundle'] ) : 'optimal';

$data = array(
'css_bundle' => in_array( $raw_bundle, Slashed_Settings::ALLOWED_BUNDLES, true ) ? $raw_bundle : 'optimal',
'integrations' => array(),
);
foreach ( Slashed_Settings::KNOWN_INTEGRATIONS as $slug ) {
$data['integrations'][ $slug ] = array_key_exists( $slug, $raw_integrations );
}

Slashed_Settings::save( $data );

wp_safe_redirect(
add_query_arg( 'slashed_saved', '1', admin_url( 'admin.php?page=' . self::PAGE_SLUG ) )
);
exit;
}

public function render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}

$settings = Slashed_Settings::get();
$integrations = $settings['integrations'];
$css_bundle = $settings['css_bundle'];
$saved = ! empty( $_GET['slashed_saved'] ); // phpcs:ignore WordPress.Security.NonceVerification
?>
<div class="wrap">
<h1><?php esc_html_e( 'SLASHED', 'slashed' ); ?> <span style="font-weight:400;font-size:13px;color:#999;"><?php echo esc_html( SLASHED_VERSION ); ?></span></h1>

<?php if ( $saved ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Settings saved.', 'slashed' ); ?></p></div>
<?php endif; ?>

<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="slashed_save_settings">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_KEY ); ?>

<h2 style="margin-top:1.5em;"><?php esc_html_e( 'Active integrations', 'slashed' ); ?></h2>
<p class="description"><?php esc_html_e( 'Enable the integrations for the builders you use. SLASHED loads its CSS and syncs design tokens only for active integrations.', 'slashed' ); ?></p>

<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e( 'Bricks Builder', 'slashed' ); ?></th>
<td>
<label>
<input type="checkbox" name="integrations[bricks]"
<?php checked( $integrations['bricks'] ); ?>>
<?php esc_html_e( 'Enable', 'slashed' ); ?>
</label>
<p class="description">
<?php
if ( defined( 'BRICKS_VERSION' ) ) {
printf(
/* translators: %s: Bricks version */
esc_html__( 'Bricks %s detected.', 'slashed' ),
esc_html( BRICKS_VERSION )
);
} else {
esc_html_e( 'Bricks Builder not detected — integration will have no effect until Bricks is installed.', 'slashed' );
}
?>
<?php if ( $integrations['bricks'] ) : ?>
&nbsp;&mdash;&nbsp;<a href="<?php echo esc_url( admin_url( 'admin.php?page=slashed-bricks' ) ); ?>"><?php esc_html_e( 'Manage Bricks token overrides →', 'slashed' ); ?></a>
<?php endif; ?>
</p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Gutenberg / Block Editor', 'slashed' ); ?></th>
<td>
<label>
<input type="checkbox" name="integrations[gutenberg]"
<?php checked( $integrations['gutenberg'] ); ?>>
<?php esc_html_e( 'Enable', 'slashed' ); ?>
</label>
<p class="description"><?php esc_html_e( 'Loads SLASHED CSS in the block editor canvas and syncs the color palette.', 'slashed' ); ?></p>
</td>
</tr>
</table>

<h2><?php esc_html_e( 'CSS bundle', 'slashed' ); ?></h2>
<p class="description"><?php esc_html_e( 'All active integrations load the same bundle. Choose the smallest bundle that covers your needs.', 'slashed' ); ?></p>

<table class="form-table" role="presentation">
<?php
$bundles = array(
'essential' => __( 'Essential — reset + design tokens only', 'slashed' ),
'optimal' => __( 'Optimal — + layout, typography, states, forms (recommended)', 'slashed' ),
'full' => __( 'Full — + utilities layer', 'slashed' ),
);
foreach ( $bundles as $value => $label ) :
?>
<tr>
<th scope="row" style="padding-top:6px;padding-bottom:6px;">
<label for="bundle-<?php echo esc_attr( $value ); ?>"><?php echo esc_html( $label ); ?></label>
</th>
<td style="padding-top:6px;padding-bottom:6px;">
<input type="radio" id="bundle-<?php echo esc_attr( $value ); ?>"
name="css_bundle" value="<?php echo esc_attr( $value ); ?>"
<?php checked( $css_bundle, $value ); ?>>
</td>
</tr>
<?php endforeach; ?>
</table>

<?php submit_button( __( 'Save settings', 'slashed' ) ); ?>
</form>

<hr>
<p style="color:#999;font-size:12px;">
<?php
printf(
/* translators: %s: CSS ref tag */
esc_html__( 'Framework: %s &middot; CSS dist SHA: %s', 'slashed' ),
esc_html( SLASHED_CSS_REF ),
'<code>' . esc_html( substr( SLASHED_DIST_SHA, 0, 8 ) ) . '</code>'
);
?>
</p>
</div>
<?php
}
}
97 changes: 97 additions & 0 deletions includes/class-css-loader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php
/**
* Shared CSS bundle resolution for all SLASHED integrations.
*
* @package SLASHED
*/

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Class Slashed_CSS_Loader
*
* Builder-agnostic helper used by every integration to locate the correct
* SLASHED CSS bundle. Integrations call these methods instead of duplicating
* CDN/local-path logic. They may then apply their own filter on top of the
* returned URL to allow per-integration overrides.
*
* In standalone mode (integration plugin activated without slashed.php) this
* class is not loaded and each integration falls back to its own inline logic.
* Adding a new integration never touches this file.
*/
class Slashed_CSS_Loader {

/**
* Get the configured CSS bundle variant.
*
* Delegates to Slashed_Settings::get_css_bundle(), which validates the
* value against the canonical allowlist and falls back to 'optimal'.
*
* @return string One of 'essential', 'optimal', 'full'.
*/
public static function get_bundle() {
return Slashed_Settings::get_css_bundle();
}

/**
* Get the URL for the SLASHED CSS bundle.
*
* Resolution order:
* 1. Local dist/ under the unified plugin root (dev / packaged-dist mode).
* 2. jsDelivr CDN pinned to the immutable dist-branch SHA.
*
* Applies the 'slashed/css_bundle_url' filter so site owners can override
* the URL globally. Integrations apply their own filter on top of this
* return value when per-integration overrides are needed.
*
* @return string
*/
public static function get_url() {
$bundle = self::get_bundle();
$filename = 'slashed.' . $bundle . '.css';

$url = sprintf(
'https://cdn.jsdelivr.net/gh/codeslash-dev/SLASHED@%s/%s',
SLASHED_DIST_SHA,
$filename
);

$local = SLASHED_PATH . 'dist/' . $filename;
if ( file_exists( $local ) ) {
$url = SLASHED_URL . 'dist/' . $filename;
}

/**
* Filter the SLASHED CSS bundle URL (all integrations).
*
* For a per-integration override, use slashed_bricks/css_bundle_url,
* slashed_gutenberg/css_bundle_url, etc. instead.
*
* @param string $url URL to the CSS bundle file.
*/
return apply_filters( 'slashed/css_bundle_url', $url );
}

/**
* Derive a cache-busting version string for a resolved CSS URL.
*
* Returns the file's mtime when the URL maps to a local file under
* SLASHED_PATH; falls back to SLASHED_VERSION for CDN or unknown URLs.
*
* @param string $url Resolved CSS bundle URL.
* @return string
*/
public static function get_version( $url ) {
// Check if URL maps to the local dist/ directory.
if ( 0 === strpos( $url, SLASHED_URL . 'dist/' ) ) {
$relative = substr( $url, strlen( SLASHED_URL . 'dist/' ) );
$path = SLASHED_PATH . 'dist/' . $relative;
if ( file_exists( $path ) ) {
return (string) filemtime( $path );
}
}
return SLASHED_VERSION;
}
}
Loading